From 8596753a639f5388d8f0ba1cfea2849f6416f42c Mon Sep 17 00:00:00 2001 From: txtsd Date: Sat, 12 Feb 2022 09:09:36 +0530 Subject: [PATCH 001/605] =?UTF-8?q?Allow=20building=20release=20builds=20a?= =?UTF-8?q?nd=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …trigger GH release using tags --- .github/workflows/build.yml | 37 +++++++---- .github/workflows/trigger_builds.yml | 91 ++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/trigger_builds.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d2ccc59e..3d0ce2a9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,12 @@ -name: build_portable +name: Build on: - [push, pull_request, workflow_dispatch] + workflow_call: + inputs: + build_type: + description: Type of build (Debug, Release, RelWithDebInfo, MinSizeRel) + type: string + default: Debug jobs: build: @@ -59,6 +64,12 @@ jobs: copy "${{ github.workspace }}\Qt\Tools\OpenSSL\Win_x86\bin\libssl-1_1.dll" "${{ github.workspace }}\${{ env.INSTALL_DIR }}\" copy "${{ github.workspace }}\Qt\Tools\OpenSSL\Win_x86\bin\libcrypto-1_1.dll" "${{ github.workspace }}\${{ env.INSTALL_DIR }}\" + - name: Set short version + shell: bash + run: | + ver_short=`git rev-parse --short HEAD` + echo "VERSION=$ver_short" >> $GITHUB_ENV + - name: Install OpenJDK uses: AdoptOpenJDK/install-jdk@v1 with: @@ -99,12 +110,12 @@ jobs: - name: Configure CMake if: runner.os != 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=Debug -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja - name: Configure CMake on Linux if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug -DLauncher_LAYOUT=lin-system -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_LAYOUT=lin-system -G Ninja - name: Build run: | @@ -124,7 +135,7 @@ jobs: if: runner.os == 'Linux' shell: bash run: | - export OUTPUT="PolyMC-${{ github.sha }}-x86_64.AppImage" + export OUTPUT="PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage" chmod +x linuxdeploy-*.AppImage @@ -145,13 +156,13 @@ jobs: - name: Run windeployqt if: runner.os == 'Windows' run: | - windeployqt --no-translations "${{ env.INSTALL_DIR }}/polymc.exe" + windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw "${{ env.INSTALL_DIR }}/polymc.exe" - name: Run macdeployqt if: runner.os == 'macOS' run: | cd ${{ env.INSTALL_DIR }} - macdeployqt "PolyMC.app" -executable="PolyMC.app/Contents/MacOS/polymc" -always-overwrite -use-debug-libs + macdeployqt "PolyMC.app" -executable="PolyMC.app/Contents/MacOS/polymc" -always-overwrite - name: chmod binary on macOS if: runner.os == 'macOS' @@ -162,25 +173,25 @@ jobs: if: runner.os == 'macOS' run: | cd ${{ env.INSTALL_DIR }} - tar -czf ../polymc.tar.gz * + tar -czf ../PolyMC.tar.gz * - name: Upload AppImage for Linux if: runner.os == 'Linux' uses: actions/upload-artifact@v2 with: - name: PolyMC-${{ github.sha }}-x86_64.AppImage - path: PolyMC-${{ github.sha }}-x86_64.AppImage + name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage + path: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage - name: Upload package for Windows if: runner.os == 'Windows' uses: actions/upload-artifact@v2 with: - name: polymc-${{ runner.os }}-${{ github.sha }}-portable + name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} path: ${{ env.INSTALL_DIR }}/** - name: Upload package for macOS if: runner.os == 'macOS' uses: actions/upload-artifact@v2 with: - name: polymc-${{ runner.os }}-${{ github.sha }}-portable - path: polymc.tar.gz + name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} + path: PolyMC.tar.gz diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml new file mode 100644 index 00000000..e50405e6 --- /dev/null +++ b/.github/workflows/trigger_builds.yml @@ -0,0 +1,91 @@ +name: Build Application + +on: + [push, pull_request, workflow_dispatch] + +jobs: + + build_debug: + name: Build Debug + uses: ./.github/workflows/build.yml + with: + build_type: Debug + + build_release: + name: Build Release + uses: ./.github/workflows/build.yml + with: + build_type: Release + + create_release: + if: startsWith(github.ref, 'refs/tags/') + needs: build_release + runs-on: ubuntu-latest + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + steps: + - name: Create release + id: create_release + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + name: PolyMC ${{ github.ref }} + draft: true + prerelease: false + + upload_release: + needs: create_release + runs-on: ubuntu-latest + steps: + + - name: Download artifacts + uses: actions/download-artifact@v2 + + - name: Grab and store version + run: | + tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") + echo "VERSION=$tag_name" >> $GITHUB_ENV + + - name: Package artifacts properly + run: | + rm -rf *Debug* + + mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage + mv PolyMC-Windows* PolyMC-Windows-${{ env.VERSION }} + mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz + + cd PolyMC-Windows-${{ env.VERSION }} + zip -r -9 ../PolyMC-Windows-${{ env.VERSION }}.zip * + cd .. + + - name: Upload Linux AppImage asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_name: PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage + asset_path: PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage + asset_content_type: application/x-executable + + - name: Upload Windows asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_name: PolyMC-Windows-${{ env.VERSION }}.zip + asset_path: PolyMC-Windows-${{ env.VERSION }}.zip + asset_content_type: application/zip + + - name: Upload macOS asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_name: PolyMC-macOS-${{ env.VERSION }}.tar.gz + asset_path: PolyMC-macOS-${{ env.VERSION }}.tar.gz + asset_content_type: application/gzip From e24a183dadb891bab633b9af3e7474a38c3b49d8 Mon Sep 17 00:00:00 2001 From: txtsd Date: Tue, 15 Feb 2022 14:38:26 +0530 Subject: [PATCH 002/605] Only trigger GH Release on stable branch --- .github/workflows/trigger_builds.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index e50405e6..7299ef1a 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -18,7 +18,7 @@ jobs: build_type: Release create_release: - if: startsWith(github.ref, 'refs/tags/') + if: contains(github.base_ref, 'refs/heads/stable') && startsWith(github.ref, 'refs/tags/') needs: build_release runs-on: ubuntu-latest outputs: From 36841eaf631ae05adb06d1bf2a3b8151623eb8d9 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 15 Feb 2022 15:03:41 +0100 Subject: [PATCH 003/605] chore(github): add issue template for RFCs --- .github/ISSUE_TEMPLATE/rfc.yml | 69 ++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/rfc.yml diff --git a/.github/ISSUE_TEMPLATE/rfc.yml b/.github/ISSUE_TEMPLATE/rfc.yml new file mode 100644 index 00000000..664430fe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/rfc.yml @@ -0,0 +1,69 @@ +# Template based on https://gitlab.archlinux.org/archlinux/rfcs/-/blob/0ba3b61e987e197f8d1901709409b8564958f78a/rfcs/0000-template.rst +name: Request for Comment (RFC) +description: Propose a larger change and start a discussion. +labels: [RFC] +body: +- type: markdown + attributes: + value: | + ### Use this form to suggest a larger change for PolyMC. +- 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 PolyMC, 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. From ff17202b431c059ae04f97148d6adcdf9c0fa86c Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 16 Feb 2022 17:03:13 +0100 Subject: [PATCH 004/605] refactor: switch to new MSA Client ID --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 74a63614..870393bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,7 +73,7 @@ set(Launcher_META_URL "https://meta.polymc.org/v1/" CACHE STRING "URL to fetch L set(Launcher_IMGUR_CLIENT_ID "5b97b0713fba4a3" CACHE STRING "Client ID you can get from Imgur when you register an application") # MSA Client ID -set(Launcher_MSA_CLIENT_ID "17b47edd-c884-4997-926d-9e7f9a6b4647" CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application") +set(Launcher_MSA_CLIENT_ID "549033b2-1532-4d4e-ae77-1bbaa46f9d74" CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application") # Bug tracker URL set(Launcher_BUG_TRACKER_URL "https://github.com/PolyMC/PolyMC/issues" CACHE STRING "URL for the bug tracker.") From 0305b7a1fd84e8986727c18475aea1f2da2adb85 Mon Sep 17 00:00:00 2001 From: dada513 Date: Thu, 17 Feb 2022 11:46:34 +0100 Subject: [PATCH 005/605] Prepare readme for 1.1.0, mark unofficial packages as unofficial --- README.md | 49 ++++++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index b7df751c..80dbc024 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,6 @@ This is a **fork** of the MultiMC Launcher and not endorsed by MultiMC. The Poly
# Installation -- All packages (archived by version) can be found [here](https://packages.polymc.org/) ([latest](https://packages.polymc.org/latest)). -- Last build status: https://jenkins.polymc.org/job/PolyMC/lastBuild/ ## 🐧 Linux @@ -29,33 +27,27 @@ There are several AUR packages available: [![polymc](https://img.shields.io/badge/aur-polymc-blue)](https://aur.archlinux.org/packages/polymc/) [![polymc-bin](https://img.shields.io/badge/aur-polymc--bin-blue)](https://aur.archlinux.org/packages/polymc-bin/) -[![polymc-git](https://img.shields.io/badge/aur-polymc--git-blue)](https://aur.archlinux.org/packages/polymc-git/) ```sh -# stable source package: +# source package: yay -S polymc -# stable binary package: +# binary package: yay -S polymc-bin -# latest git package: -yay -S polymc-git ``` -### Debian +### Debian and Ubuntu We use [makedeb](https://docs.makedeb.org/) for our Debian packages. Several MPR packages are available: [![polymc](https://img.shields.io/badge/mpr-polymc-orange)](https://mpr.makedeb.org/packages/polymc) [![polymc-bin](https://img.shields.io/badge/mpr-polymc--bin-orange)](https://mpr.makedeb.org/packages/polymc-bin) -[![polymc-git](https://img.shields.io/badge/mpr-polymc--git-orange)](https://mpr.makedeb.org/packages/polymc-git) ```sh -# stable source package: +# source package: sudo tap install polymc -# stable binary package: +# binary package: sudo tap install polymc-bin -# latest git package: -sudo tap install polymc-git ``` ### Nix @@ -85,13 +77,6 @@ sudo dnf copr enable polymc/polymc sudo dnf install polymc ``` -Alternatively, a COPR maintained by a PolyMC user (instead of Jenkins' automated builds) is available [here](https://copr.fedorainfracloud.org/coprs/sentry/polymc). - -```sh -sudo dnf copr enable sentry/polymc -sudo dnf install polymc -``` - ### Slackware [A SlackBuild](https://codeberg.org/glowiak/SlackBuilds/src/branch/master/repository/polymc.md) is available. You will need [qt5](http://slackbuilds.org/repository/14.2/libraries/qt5/) (on 15.0 installed by default), [a JDK](https://codeberg.org/glowiak/SlackBuilds/src/branch/master/repository/adoptium-jdk8.md), and if you're on 14.2, you need to compile newer CMake version manually. To build, type in extracted directory with all dependiences met: @@ -99,7 +84,7 @@ sudo dnf install polymc sudo ./polymc.SlackBuild sudo installpkg /tmp/polymc-version-arch-1_SBo.tgz -If you are too lazy to do all these steps, you can just download [a prebuild x86_64 package](http://glowiak.github.io/file/polymc-latest-slackware) and install it with /sbin/installpkg: +You can also download an unofficial [prebuilt x86_64 package](http://glowiak.github.io/file/polymc-latest-slackware) and install it with /sbin/installpkg: sudo /sbin/installpkg ~/Downloads/polymc-version-x86_64-1_SBo.tgz @@ -109,28 +94,37 @@ If you are too lazy to do all these steps, you can just download [a prebuild x86 ## MacOS -MacOS currently does not have any packages. We are still working on setting up MacOS packaging. Meanwhile, you can [build](https://github.com/PolyMC/PolyMC/blob/develop/BUILD.md#macos) it for yourself. +MacOS has experimental development builds available [here](https://github.com/PolyMC/PolyMC/actions) ## FreeBSD -For FreeBSD available are: +There are unofficial binary packages available: - [AppBSD Image](http://glowiak.github.io/file/polymc-latest-fbsd64-appbsd) - a portable application, requires [AppBSD](https://codeberg.org/glowiak/appbsd/) to be installed. - [Gzipped binaries](http://glowiak.github.io/file/polymc-latest-fbsd64-raw) - traditional way to distribute, unpack and run. -In both cases you need X11, Qt5 and Java installed. Both files are 64bit only. +In both cases you need X11, Qt5 and Java installed. Both files are 64bit only. +You can build from source - see [BUILD.md](./BUILD.md) ## OpenBSD -For OpenBSD available are [gzipped 32-bit binaries](http://glowiak.github.io/file/polymc-latest-obsd32-raw), download, unpack and run. +There are unofficial binary packages available: -You need X11, Qt5 and Java installed. +- [gzipped 32-bit binaries](http://glowiak.github.io/file/polymc-latest-obsd32-raw), download, unpack and run. + +You need X11, Qt5 and Java installed. +You can build from source - see [BUILD.md](./BUILD.md) ## Development Builds There are per-commit development builds available [here](https://github.com/PolyMC/PolyMC/actions). These have debug information in the binaries, so their file sizes are relatively larger. -Builds are provided for Linux, AppImage on Linux, Windows, and macOS. +Portable builds are provided for AppImage on Linux, Windows, and macOS. + +For Debian and Arch, you can use these packages for the latest development versions: +[![polymc-git](https://img.shields.io/badge/aur-polymc--git-blue)](https://aur.archlinux.org/packages/polymc-git/) +[![polymc-git](https://img.shields.io/badge/mpr-polymc--git-orange)](https://mpr.makedeb.org/packages/polymc-git) +For flatpak, you can use [flathub-beta](https://discourse.flathub.org/t/how-to-use-flathub-beta/2111) # Help & Support @@ -158,6 +152,7 @@ If you want to contribute to PolyMC you might find it useful to join our Discord If you want to build PolyMC yourself, check [BUILD.md](BUILD.md) for build instructions. ## Code formatting + Just follow the existing formatting. In general, in order of importance: From 6d7676202f3b1b8d2949b23bb56cfcc2daa3a728 Mon Sep 17 00:00:00 2001 From: dada513 Date: Thu, 17 Feb 2022 12:53:44 +0100 Subject: [PATCH 006/605] unofficial => community maintained --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 80dbc024..aec951b0 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ sudo dnf install polymc sudo ./polymc.SlackBuild sudo installpkg /tmp/polymc-version-arch-1_SBo.tgz -You can also download an unofficial [prebuilt x86_64 package](http://glowiak.github.io/file/polymc-latest-slackware) and install it with /sbin/installpkg: +You can also download a community-maintained [prebuilt x86_64 package](http://glowiak.github.io/file/polymc-latest-slackware) and install it with /sbin/installpkg: sudo /sbin/installpkg ~/Downloads/polymc-version-x86_64-1_SBo.tgz @@ -98,7 +98,7 @@ MacOS has experimental development builds available [here](https://github.com/Po ## FreeBSD -There are unofficial binary packages available: +There are community-maintained binary packages available: - [AppBSD Image](http://glowiak.github.io/file/polymc-latest-fbsd64-appbsd) - a portable application, requires [AppBSD](https://codeberg.org/glowiak/appbsd/) to be installed. @@ -109,7 +109,7 @@ You can build from source - see [BUILD.md](./BUILD.md) ## OpenBSD -There are unofficial binary packages available: +There are community-maintained packages available: - [gzipped 32-bit binaries](http://glowiak.github.io/file/polymc-latest-obsd32-raw), download, unpack and run. From 107a0ea85283c9604e0b1e118c6948a7afd008d9 Mon Sep 17 00:00:00 2001 From: dada513 Date: Thu, 17 Feb 2022 13:19:35 +0100 Subject: [PATCH 007/605] fix2 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aec951b0..cf8c1a5f 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ You can build from source - see [BUILD.md](./BUILD.md) ## OpenBSD -There are community-maintained packages available: +There are community-maintained binary packages available: - [gzipped 32-bit binaries](http://glowiak.github.io/file/polymc-latest-obsd32-raw), download, unpack and run. From 10de75623ecd276a56c51bc5c11166f5e94a7b4c Mon Sep 17 00:00:00 2001 From: dada513 Date: Fri, 18 Feb 2022 10:28:17 +0100 Subject: [PATCH 008/605] readd all packages --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index cf8c1a5f..50bd88b9 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ This is a **fork** of the MultiMC Launcher and not endorsed by MultiMC. The Poly # Installation +- All packages (archived by version) can be found [here](https://packages.polymc.org/) ([latest](https://packages.polymc.org/latest)) +- Last build status: https://jenkins.polymc.org/job/PolyMC/lastBuild/ + ## 🐧 Linux ### Cross-distro packages From be910374dc81225863a55c1ada23074b0cb5f219 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 18 Feb 2022 12:25:26 +0100 Subject: [PATCH 009/605] feat(accounts): support msa-client-id value --- launcher/minecraft/auth/AccountData.cpp | 5 +++++ launcher/minecraft/auth/AccountData.h | 1 + 2 files changed, 6 insertions(+) diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 9b84fe1a..f791db14 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -327,6 +327,10 @@ bool AccountData::resumeStateFromV3(QJsonObject data) { } if(type == AccountType::MSA) { + auto clientIDV = data.value("msa-client-id"); + if (clientIDV.isString()) { + msaClientID = clientIDV.toString(); + } // leave msaClientID empty if it doesn't exist or isn't a string msaToken = tokenFromJSONV3(data, "msa"); userToken = tokenFromJSONV3(data, "utoken"); xboxApiToken = tokenFromJSONV3(data, "xrp-main"); @@ -360,6 +364,7 @@ QJsonObject AccountData::saveState() const { } else if (type == AccountType::MSA) { output["type"] = "MSA"; + output["msa-client-id"] = msaClientID; tokenToJSONV3(output, msaToken, "msa"); tokenToJSONV3(output, userToken, "utoken"); tokenToJSONV3(output, xboxApiToken, "xrp-main"); diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 606c1ad1..1b6867de 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -81,6 +81,7 @@ struct AccountData { bool legacy = false; bool canMigrateToMSA = false; + QString msaClientID; Katabasis::Token msaToken; Katabasis::Token userToken; Katabasis::Token xboxApiToken; From 9c71f364d25df5a992c7067ecfca2e095abcc20f Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 18 Feb 2022 12:26:52 +0100 Subject: [PATCH 010/605] feat(accounts): add disabled account state --- launcher/LaunchController.cpp | 12 ++++++++++++ launcher/minecraft/auth/AccountData.h | 1 + launcher/minecraft/auth/AccountList.cpp | 3 +++ launcher/minecraft/auth/AccountTask.cpp | 8 ++++++++ launcher/minecraft/auth/AccountTask.h | 1 + launcher/minecraft/auth/MinecraftAccount.cpp | 3 +++ 6 files changed, 28 insertions(+) diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 32fc99cb..11419358 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -228,6 +228,18 @@ void LaunchController::login() { emitFailed(errorString); return; } + case AccountState::Disabled: { + auto errorString = tr("The launcher's client identification changed. Please remove this account and add it again."); + QMessageBox::warning( + m_parentWidget, + tr("Client identification changed"), + errorString, + QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok + ); + emitFailed(errorString); + return; + } case AccountState::Gone: { auto errorString = tr("The account no longer exists on the servers. It may have been migrated, in which case please add the new account you migrated this one to."); QMessageBox::warning( diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 1b6867de..6749a471 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -47,6 +47,7 @@ enum class AccountState { Offline, Working, Online, + Disabled, Errored, Expired, Gone diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index 04470e1c..e404cdda 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -291,6 +291,9 @@ QVariant AccountList::data(const QModelIndex &index, int role) const case AccountState::Expired: { return tr("Expired", "Account status"); } + case AccountState::Disabled: { + return tr("Disabled", "Account status"); + } case AccountState::Gone: { return tr("Gone", "Account status"); } diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp index 98d8d94d..321b350f 100644 --- a/launcher/minecraft/auth/AccountTask.cpp +++ b/launcher/minecraft/auth/AccountTask.cpp @@ -43,6 +43,8 @@ QString AccountTask::getStateMessage() const return tr("Authentication task succeeded."); case AccountTaskState::STATE_OFFLINE: return tr("Failed to contact the authentication server."); + case AccountTaskState::STATE_DISABLED: + return tr("Client ID has changed. New session needs to be created."); case AccountTaskState::STATE_FAILED_SOFT: return tr("Encountered an error during authentication."); case AccountTaskState::STATE_FAILED_HARD: @@ -78,6 +80,12 @@ bool AccountTask::changeState(AccountTaskState newState, QString reason) emitFailed(reason); return false; } + case AccountTaskState::STATE_DISABLED: { + m_data->errorString = reason; + m_data->accountState = AccountState::Disabled; + emitFailed(reason); + return false; + } case AccountTaskState::STATE_FAILED_SOFT: { m_data->errorString = reason; m_data->accountState = AccountState::Errored; diff --git a/launcher/minecraft/auth/AccountTask.h b/launcher/minecraft/auth/AccountTask.h index dac3f1b5..c2a5d86c 100644 --- a/launcher/minecraft/auth/AccountTask.h +++ b/launcher/minecraft/auth/AccountTask.h @@ -35,6 +35,7 @@ enum class AccountTaskState STATE_CREATED, STATE_WORKING, STATE_SUCCEEDED, + STATE_DISABLED, //!< MSA Client ID has changed. Tell user to reloginn STATE_FAILED_SOFT, //!< soft failure. authentication went through partially STATE_FAILED_HARD, //!< hard failure. main tokens are invalid STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index ffc81ed8..a604cadf 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -176,6 +176,9 @@ void MinecraftAccount::authFailed(QString reason) { switch (m_currentTask->taskState()) { case AccountTaskState::STATE_OFFLINE: + case AccountTaskState::STATE_DISABLED: { + // NOTE: user will need to fix this themselves. + } case AccountTaskState::STATE_FAILED_SOFT: { // NOTE: this doesn't do much. There was an error of some sort. } From 14717396eb9be5f14b23a1af50e1379e66cfaf3c Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 18 Feb 2022 12:27:34 +0100 Subject: [PATCH 011/605] feat(accounts): save client id in MSAStep --- launcher/minecraft/auth/steps/MSAStep.cpp | 4 +++- launcher/minecraft/auth/steps/MSAStep.h | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index 779aee43..7d28c2c8 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -12,9 +12,10 @@ using OAuth2 = Katabasis::DeviceFlow; using Activity = Katabasis::Activity; MSAStep::MSAStep(AccountData* data, Action action) : AuthStep(data), m_action(action) { + m_clientId = APPLICATION->getMSAClientID(); OAuth2::Options opts; opts.scope = "XboxLive.signin offline_access"; - opts.clientIdentifier = APPLICATION->getMSAClientID(); + opts.clientIdentifier = m_clientId; opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; @@ -57,6 +58,7 @@ void MSAStep::perform() { m_oauth2->setExtraRequestParams(extraOpts); *m_data = AccountData(); + m_data->msaClientID = m_clientId; m_oauth2->login(); return; } diff --git a/launcher/minecraft/auth/steps/MSAStep.h b/launcher/minecraft/auth/steps/MSAStep.h index 49ba3542..301e1465 100644 --- a/launcher/minecraft/auth/steps/MSAStep.h +++ b/launcher/minecraft/auth/steps/MSAStep.h @@ -29,4 +29,5 @@ private slots: private: Katabasis::DeviceFlow *m_oauth2 = nullptr; Action m_action; + QString m_clientId; }; From c5d9944993832f16e76a9a63a402665c5321267b Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 18 Feb 2022 12:27:57 +0100 Subject: [PATCH 012/605] feat(accounts): interrupt MSAStep when client ID doesn't match --- launcher/minecraft/auth/steps/MSAStep.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index 7d28c2c8..207d9373 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -49,6 +49,10 @@ void MSAStep::rehydrate() { void MSAStep::perform() { switch(m_action) { case Refresh: { + if (m_data->msaClientID != m_clientId) { + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_DISABLED, tr("Microsoft user authentication failed - client identification has changed.")); + } m_oauth2->refresh(); return; } From 9b7cd029a79f42456c0907014737b5cdd62fc657 Mon Sep 17 00:00:00 2001 From: txtsd Date: Fri, 18 Feb 2022 19:27:15 +0530 Subject: [PATCH 013/605] Grab short version --- .github/workflows/trigger_builds.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 7299ef1a..4a162665 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -24,6 +24,10 @@ jobs: outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: + - name: Grab and store version + run: | + tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") + echo "VERSION=$tag_name" >> $GITHUB_ENV - name: Create release id: create_release uses: softprops/action-gh-release@v1 @@ -31,7 +35,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} - name: PolyMC ${{ github.ref }} + name: PolyMC ${{ env.VERSION }} draft: true prerelease: false From 80a29af497bec487f87a9ae545a2f94c3685f6df Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 18 Feb 2022 19:18:29 +0100 Subject: [PATCH 014/605] fix: typo for account disabled error messages --- launcher/LaunchController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 11419358..40178b70 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -229,7 +229,7 @@ void LaunchController::login() { return; } case AccountState::Disabled: { - auto errorString = tr("The launcher's client identification changed. Please remove this account and add it again."); + auto errorString = tr("The launcher's client identification has changed. Please remove this account and add it again."); QMessageBox::warning( m_parentWidget, tr("Client identification changed"), From adacab33494f307a93e41ac0eee4fdd226c39614 Mon Sep 17 00:00:00 2001 From: timoreo Date: Sat, 19 Feb 2022 15:17:45 +0100 Subject: [PATCH 015/605] Fixed segfault when closing window while version info download is still going --- .../ui/pages/modplatform/flame/FlameModPage.cpp | 13 ++++++++----- .../ui/pages/modplatform/modrinth/ModrinthPage.cpp | 11 +++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index a816c681..4afdd142 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -113,13 +113,12 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) { qDebug() << "Loading flame mod versions"; auto netJob = new NetJob(QString("Flame::ModVersions(%1)").arg(current.name), APPLICATION->network()); - std::shared_ptr response = std::make_shared(); + auto response = new QByteArray(); int addonId = current.addonId; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response.get())); + netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, netJob] + QObject::connect(netJob, &NetJob::succeeded, this, [this, response] { - netJob->deleteLater(); QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { @@ -150,9 +149,13 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) if(ui->versionSelectionBox->count() == 0){ ui->versionSelectionBox->addItem(tr("No Valid Version found!"), QVariant(-1)); } - suggestCurrent(); }); + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] + { + netJob->deleteLater(); + delete response; + }); netJob->start(); } else diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index c5a54c29..577a7bcb 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -98,13 +98,12 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { qDebug() << "Loading Modrinth mod versions"; auto netJob = new NetJob(QString("Modrinth::ModVersions(%1)").arg(current.name), APPLICATION->network()); - std::shared_ptr response = std::make_shared(); + auto response = new QByteArray(); QString addonId = current.addonId; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId), response.get())); + netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId), response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, netJob] + QObject::connect(netJob, &NetJob::succeeded, this, [this, response] { - netJob->deleteLater(); QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { @@ -138,6 +137,10 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) suggestCurrent(); }); + QObject::connect(netJob, &NetJob::finished, this, [response, netJob]{ + netJob->deleteLater(); + delete response; + }); netJob->start(); } else From 8556ff5eac8a5a880896e954081e5c9ada224eaa Mon Sep 17 00:00:00 2001 From: Glitch Date: Sun, 20 Feb 2022 14:56:45 -0600 Subject: [PATCH 016/605] Revert ba6a97557a0d90d77e9eba560931414e39042447 Let evil win. --- launcher/Application.cpp | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index e916dcf7..e33df252 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -192,27 +192,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) #endif startTime = QDateTime::currentDateTime(); -#ifdef Q_OS_LINUX - { - QFile osrelease("/proc/sys/kernel/osrelease"); - if (osrelease.open(QFile::ReadOnly | QFile::Text)) { - QTextStream in(&osrelease); - auto contents = in.readAll(); - if( - contents.contains("WSL", Qt::CaseInsensitive) || - contents.contains("Microsoft", Qt::CaseInsensitive) - ) { - showFatalErrorMessage( - "Unsupported system detected!", - "Linux-on-Windows distributions are not supported.\n\n" - "Please use the Windows binary when playing on Windows." - ); - return; - } - } - } -#endif - // Don't quit on hiding the last window this->setQuitOnLastWindowClosed(false); From da70122d9c46c6281c06fe1dedc558c13077f64f Mon Sep 17 00:00:00 2001 From: swirl Date: Sun, 20 Feb 2022 19:23:08 -0500 Subject: [PATCH 017/605] remove notifications --- CMakeLists.txt | 5 +- buildconfig/BuildConfig.cpp.in | 2 - buildconfig/BuildConfig.h | 7 - launcher/Application.cpp | 5 +- launcher/CMakeLists.txt | 11 -- .../notifications/NotificationChecker.cpp | 129 ------------------ launcher/notifications/NotificationChecker.h | 61 --------- launcher/ui/MainWindow.cpp | 31 ----- launcher/ui/MainWindow.h | 4 - launcher/ui/dialogs/NotificationDialog.cpp | 86 ------------ launcher/ui/dialogs/NotificationDialog.h | 44 ------ launcher/ui/dialogs/NotificationDialog.ui | 85 ------------ launcher/ui/pages/global/LauncherPage.cpp | 5 - launcher/ui/pages/global/LauncherPage.ui | 22 +-- 14 files changed, 3 insertions(+), 494 deletions(-) delete mode 100644 launcher/notifications/NotificationChecker.cpp delete mode 100644 launcher/notifications/NotificationChecker.h delete mode 100644 launcher/ui/dialogs/NotificationDialog.cpp delete mode 100644 launcher/ui/dialogs/NotificationDialog.h delete mode 100644 launcher/ui/dialogs/NotificationDialog.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index d4a27260..6365f4c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,14 +58,11 @@ set(Launcher_VERSION_HOTFIX 6) set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") # Build platform. -set(Launcher_BUILD_PLATFORM "" CACHE STRING "A short string identifying the platform that this build was built for. Only used by the notification system and to display in the about dialog.") +set(Launcher_BUILD_PLATFORM "" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") # Channel list URL set(Launcher_UPDATER_BASE "" CACHE STRING "Base URL for the updater.") -# Notification URL -set(Launcher_NOTIFICATION_URL "" CACHE STRING "URL for checking for notifications.") - # The metadata server set(Launcher_META_URL "https://meta.polymc.org/v1/" CACHE STRING "URL to fetch Launcher's meta files from.") diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 0ffc9326..ed6a755a 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -25,8 +25,6 @@ Config::Config() BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; UPDATER_BASE = "@Launcher_UPDATER_BASE@"; - NOTIFICATION_URL = "@Launcher_NOTIFICATION_URL@"; - FULL_VERSION_STR = "@Launcher_VERSION_MAJOR@.@Launcher_VERSION_MINOR@.@Launcher_VERSION_BUILD@"; GIT_COMMIT = "@Launcher_GIT_COMMIT@"; GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 111381ab..a5c3c859 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -46,13 +46,6 @@ public: /// User-Agent to use for uncached requests. QString USER_AGENT_UNCACHED; - - /// URL for notifications - QString NOTIFICATION_URL; - - /// Used for matching notifications - QString FULL_VERSION_STR; - /// The git commit hash of this build QString GIT_COMMIT; diff --git a/launcher/Application.cpp b/launcher/Application.cpp index e916dcf7..ce40c4f3 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -590,9 +590,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("IconTheme", QString("pe_colored")); m_settings->registerSetting("ApplicationTheme", QString("system")); - // Notifications - m_settings->registerSetting("ShownNotifications", QString()); - // Remembered state m_settings->registerSetting("LastUsedGroupForNewInstance", QString()); @@ -1512,7 +1509,7 @@ QString Application::getJarsPath() return m_jarsPath; } -QString Application::getMSAClientID() +QString Application::getMSAClientID() { QString clientIDOverride = m_settings->get("MSAClientIDOverride").toString(); if (!clientIDOverride.isEmpty()) { diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 90149c3b..51a63722 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -174,13 +174,6 @@ add_unit_test(DownloadTask DATA updater/testdata ) -# Rarely used notifications -set(NOTIFICATIONS_SOURCES - # Notifications - short warning messages - notifications/NotificationChecker.h - notifications/NotificationChecker.cpp -) - # Backend for the news bar... there's usually no news. set(NEWS_SOURCES # News System @@ -568,7 +561,6 @@ set(LOGIC_SOURCES ${NET_SOURCES} ${LAUNCH_SOURCES} ${UPDATE_SOURCES} - ${NOTIFICATIONS_SOURCES} ${NEWS_SOURCES} ${MINECRAFT_SOURCES} ${SCREENSHOTS_SOURCES} @@ -800,8 +792,6 @@ SET(LAUNCHER_SOURCES ui/dialogs/NewComponentDialog.h ui/dialogs/NewInstanceDialog.cpp ui/dialogs/NewInstanceDialog.h - ui/dialogs/NotificationDialog.cpp - ui/dialogs/NotificationDialog.h ui/pagedialog/PageDialog.cpp ui/pagedialog/PageDialog.h ui/dialogs/ProgressDialog.cpp @@ -903,7 +893,6 @@ qt5_wrap_ui(LAUNCHER_UI ui/dialogs/ProfileSetupDialog.ui ui/dialogs/ProgressDialog.ui ui/dialogs/NewInstanceDialog.ui - ui/dialogs/NotificationDialog.ui ui/dialogs/UpdateDialog.ui ui/dialogs/NewComponentDialog.ui ui/dialogs/ProfileSelectDialog.ui diff --git a/launcher/notifications/NotificationChecker.cpp b/launcher/notifications/NotificationChecker.cpp deleted file mode 100644 index 10b91691..00000000 --- a/launcher/notifications/NotificationChecker.cpp +++ /dev/null @@ -1,129 +0,0 @@ -#include "NotificationChecker.h" - -#include -#include -#include -#include - -#include "net/Download.h" - -#include "Application.h" - -NotificationChecker::NotificationChecker(QObject *parent) - : QObject(parent) -{ -} - -void NotificationChecker::setNotificationsUrl(const QUrl ¬ificationsUrl) -{ - m_notificationsUrl = notificationsUrl; -} - -void NotificationChecker::setApplicationChannel(QString channel) -{ - m_appVersionChannel = channel; -} - -void NotificationChecker::setApplicationFullVersion(QString version) -{ - m_appFullVersion = version; -} - -void NotificationChecker::setApplicationPlatform(QString platform) -{ - m_appPlatform = platform; -} - -QList NotificationChecker::notificationEntries() const -{ - return m_entries; -} - -void NotificationChecker::checkForNotifications() -{ - if (!m_notificationsUrl.isValid()) - { - qCritical() << "Failed to check for notifications. No notifications URL set." - << "If you'd like to use PolyMC's notification system, please pass the " - "URL to CMake at compile time."; - return; - } - if (m_checkJob) - { - return; - } - m_checkJob = new NetJob("Checking for notifications", APPLICATION->network()); - auto entry = APPLICATION->metacache()->resolveEntry("root", "notifications.json"); - entry->setStale(true); - m_checkJob->addNetAction(m_download = Net::Download::makeCached(m_notificationsUrl, entry)); - connect(m_download.get(), &Net::Download::succeeded, this, &NotificationChecker::downloadSucceeded); - m_checkJob->start(); -} - -void NotificationChecker::downloadSucceeded(int) -{ - m_entries.clear(); - - QFile file(m_download->getTargetFilepath()); - if (file.open(QFile::ReadOnly)) - { - QJsonArray root = QJsonDocument::fromJson(file.readAll()).array(); - for (auto it = root.begin(); it != root.end(); ++it) - { - QJsonObject obj = (*it).toObject(); - NotificationEntry entry; - entry.id = obj.value("id").toDouble(); - entry.message = obj.value("message").toString(); - entry.channel = obj.value("channel").toString(); - entry.platform = obj.value("platform").toString(); - entry.from = obj.value("from").toString(); - entry.to = obj.value("to").toString(); - const QString type = obj.value("type").toString("critical"); - if (type == "critical") - { - entry.type = NotificationEntry::Critical; - } - else if (type == "warning") - { - entry.type = NotificationEntry::Warning; - } - else if (type == "information") - { - entry.type = NotificationEntry::Information; - } - if(entryApplies(entry)) - m_entries.append(entry); - } - } - - m_checkJob.reset(); - - emit notificationCheckFinished(); -} - -bool versionLessThan(const QString &v1, const QString &v2) -{ - QStringList l1 = v1.split('.'); - QStringList l2 = v2.split('.'); - while (!l1.isEmpty() && !l2.isEmpty()) - { - int one = l1.isEmpty() ? 0 : l1.takeFirst().toInt(); - int two = l2.isEmpty() ? 0 : l2.takeFirst().toInt(); - if (one != two) - { - return one < two; - } - } - return false; -} - -bool NotificationChecker::entryApplies(const NotificationChecker::NotificationEntry& entry) const -{ - bool channelApplies = entry.channel.isEmpty() || entry.channel == m_appVersionChannel; - bool platformApplies = entry.platform.isEmpty() || entry.platform == m_appPlatform; - bool fromApplies = - entry.from.isEmpty() || entry.from == m_appFullVersion || !versionLessThan(m_appFullVersion, entry.from); - bool toApplies = - entry.to.isEmpty() || entry.to == m_appFullVersion || !versionLessThan(entry.to, m_appFullVersion); - return channelApplies && platformApplies && fromApplies && toApplies; -} diff --git a/launcher/notifications/NotificationChecker.h b/launcher/notifications/NotificationChecker.h deleted file mode 100644 index 0f305f33..00000000 --- a/launcher/notifications/NotificationChecker.h +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#include - -#include "net/NetJob.h" -#include "net/Download.h" - -class NotificationChecker : public QObject -{ - Q_OBJECT - -public: - explicit NotificationChecker(QObject *parent = 0); - - void setNotificationsUrl(const QUrl ¬ificationsUrl); - void setApplicationPlatform(QString platform); - void setApplicationChannel(QString channel); - void setApplicationFullVersion(QString version); - - struct NotificationEntry - { - int id; - QString message; - enum - { - Critical, - Warning, - Information - } type; - QString channel; - QString platform; - QString from; - QString to; - }; - - QList notificationEntries() const; - -public -slots: - void checkForNotifications(); - -private -slots: - void downloadSucceeded(int); - -signals: - void notificationCheckFinished(); - -private: - bool entryApplies(const NotificationEntry &entry) const; - -private: - QList m_entries; - QUrl m_notificationsUrl; - NetJob::Ptr m_checkJob; - Net::Download::Ptr m_download; - - QString m_appVersionChannel; - QString m_appPlatform; - QString m_appFullVersion; -}; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index ad7227cc..a3da64e4 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -59,7 +59,6 @@ #include #include #include -#include #include #include #include @@ -82,7 +81,6 @@ #include "ui/dialogs/CopyInstanceDialog.h" #include "ui/dialogs/UpdateDialog.h" #include "ui/dialogs/EditAccountDialog.h" -#include "ui/dialogs/NotificationDialog.h" #include "ui/dialogs/ExportInstanceDialog.h" #include "UpdateController.h" @@ -835,17 +833,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow } } - { - auto checker = new NotificationChecker(); - checker->setNotificationsUrl(QUrl(BuildConfig.NOTIFICATION_URL)); - checker->setApplicationChannel(BuildConfig.VERSION_CHANNEL); - checker->setApplicationPlatform(BuildConfig.BUILD_PLATFORM); - checker->setApplicationFullVersion(BuildConfig.FULL_VERSION_STR); - m_notificationChecker.reset(checker); - connect(m_notificationChecker.get(), &NotificationChecker::notificationCheckFinished, this, &MainWindow::notificationsChanged); - checker->checkForNotifications(); - } - setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString()); // removing this looks stupid @@ -1257,24 +1244,6 @@ QString intListToString(const QList &list) } return slist.join(','); } -void MainWindow::notificationsChanged() -{ - QList entries = m_notificationChecker->notificationEntries(); - QList shownNotifications = stringToIntList(APPLICATION->settings()->get("ShownNotifications").toString()); - for (auto it = entries.begin(); it != entries.end(); ++it) - { - NotificationChecker::NotificationEntry entry = *it; - if (!shownNotifications.contains(entry.id)) - { - NotificationDialog dialog(entry, this); - if (dialog.exec() == NotificationDialog::DontShowAgain) - { - shownNotifications.append(entry.id); - } - } - } - APPLICATION->settings()->set("ShownNotifications", intListToString(shownNotifications)); -} void MainWindow::downloadUpdates(GoUpdate::Status status) { diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index f6940ab0..fd65620e 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -28,7 +28,6 @@ class LaunchController; class NewsChecker; -class NotificationChecker; class QToolButton; class InstanceProxyModel; class LabeledToolButton; @@ -166,8 +165,6 @@ private slots: void updateNotAvailable(); - void notificationsChanged(); - void defaultAccountChanged(); void changeActiveAccount(); @@ -213,7 +210,6 @@ private: KonamiCode * secretEventFilter = nullptr; unique_qobject_ptr m_newsChecker; - unique_qobject_ptr m_notificationChecker; InstancePtr m_selectedInstance; QString m_currentInstIcon; diff --git a/launcher/ui/dialogs/NotificationDialog.cpp b/launcher/ui/dialogs/NotificationDialog.cpp deleted file mode 100644 index f2a35ae2..00000000 --- a/launcher/ui/dialogs/NotificationDialog.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#include "NotificationDialog.h" -#include "ui_NotificationDialog.h" - -#include -#include - -NotificationDialog::NotificationDialog(const NotificationChecker::NotificationEntry &entry, QWidget *parent) : - QDialog(parent, Qt::MSWindowsFixedSizeDialogHint | Qt::WindowTitleHint | Qt::CustomizeWindowHint), - ui(new Ui::NotificationDialog) -{ - ui->setupUi(this); - - QStyle::StandardPixmap icon; - switch (entry.type) - { - case NotificationChecker::NotificationEntry::Critical: - icon = QStyle::SP_MessageBoxCritical; - break; - case NotificationChecker::NotificationEntry::Warning: - icon = QStyle::SP_MessageBoxWarning; - break; - default: - case NotificationChecker::NotificationEntry::Information: - icon = QStyle::SP_MessageBoxInformation; - break; - } - ui->iconLabel->setPixmap(style()->standardPixmap(icon, 0, this)); - ui->messageLabel->setText(entry.message); - - m_dontShowAgainText = tr("Don't show again"); - m_closeText = tr("Close"); - - ui->dontShowAgainBtn->setText(m_dontShowAgainText + QString(" (%1)").arg(m_dontShowAgainTime)); - ui->closeBtn->setText(m_closeText + QString(" (%1)").arg(m_closeTime)); - - startTimer(1000); -} - -NotificationDialog::~NotificationDialog() -{ - delete ui; -} - -void NotificationDialog::timerEvent(QTimerEvent *event) -{ - if (m_dontShowAgainTime > 0) - { - m_dontShowAgainTime--; - if (m_dontShowAgainTime == 0) - { - ui->dontShowAgainBtn->setText(m_dontShowAgainText); - ui->dontShowAgainBtn->setEnabled(true); - } - else - { - ui->dontShowAgainBtn->setText(m_dontShowAgainText + QString(" (%1)").arg(m_dontShowAgainTime)); - } - } - if (m_closeTime > 0) - { - m_closeTime--; - if (m_closeTime == 0) - { - ui->closeBtn->setText(m_closeText); - ui->closeBtn->setEnabled(true); - } - else - { - ui->closeBtn->setText(m_closeText + QString(" (%1)").arg(m_closeTime)); - } - } - - if (m_closeTime == 0 && m_dontShowAgainTime == 0) - { - killTimer(event->timerId()); - } -} - -void NotificationDialog::on_dontShowAgainBtn_clicked() -{ - done(DontShowAgain); -} -void NotificationDialog::on_closeBtn_clicked() -{ - done(Normal); -} diff --git a/launcher/ui/dialogs/NotificationDialog.h b/launcher/ui/dialogs/NotificationDialog.h deleted file mode 100644 index e1cbb9fa..00000000 --- a/launcher/ui/dialogs/NotificationDialog.h +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef NOTIFICATIONDIALOG_H -#define NOTIFICATIONDIALOG_H - -#include - -#include "notifications/NotificationChecker.h" - -namespace Ui { -class NotificationDialog; -} - -class NotificationDialog : public QDialog -{ - Q_OBJECT - -public: - explicit NotificationDialog(const NotificationChecker::NotificationEntry &entry, QWidget *parent = 0); - ~NotificationDialog(); - - enum ExitCode - { - Normal, - DontShowAgain - }; - -protected: - void timerEvent(QTimerEvent *event); - -private: - Ui::NotificationDialog *ui; - - int m_dontShowAgainTime = 10; - int m_closeTime = 5; - - QString m_dontShowAgainText; - QString m_closeText; - -private -slots: - void on_dontShowAgainBtn_clicked(); - void on_closeBtn_clicked(); -}; - -#endif // NOTIFICATIONDIALOG_H diff --git a/launcher/ui/dialogs/NotificationDialog.ui b/launcher/ui/dialogs/NotificationDialog.ui deleted file mode 100644 index 3e6c22bc..00000000 --- a/launcher/ui/dialogs/NotificationDialog.ui +++ /dev/null @@ -1,85 +0,0 @@ - - - NotificationDialog - - - - 0 - 0 - 320 - 240 - - - - Notification - - - - - - - - TextLabel - - - - - - - TextLabel - - - true - - - true - - - Qt::TextBrowserInteraction - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - false - - - Don't show again - - - - - - - false - - - Close - - - - - - - - - - diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 0ffe8050..9e1d761b 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -234,11 +234,6 @@ void LauncherPage::applySettings() { auto s = APPLICATION->settings(); - if (ui->resetNotificationsBtn->isChecked()) - { - s->set("ShownNotifications", QString()); - } - // Updates s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked()); s->set("UpdateChannel", m_currentUpdateChannel); diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 47fed873..3722072d 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -38,7 +38,7 @@ QTabWidget::Rounded - 0 + 1 @@ -184,25 +184,6 @@ User Interface - - - - Launcher notifications - - - - - - Reset hidden notifications - - - true - - - - - - @@ -499,7 +480,6 @@ modsDirBrowseBtn iconsDirTextBox iconsDirBrowseBtn - resetNotificationsBtn sortLastLaunchedBtn sortByNameBtn themeComboBox From 5bbb4f31dcc53b251b9f5836e73fff64abe91970 Mon Sep 17 00:00:00 2001 From: txtsd Date: Sun, 20 Feb 2022 01:16:43 +0530 Subject: [PATCH 018/605] Add generic Linux system builds --- .github/workflows/build.yml | 30 ++++++++++++++++++++++------ .github/workflows/trigger_builds.yml | 11 ++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d0ce2a9..dbfcb82f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,9 +15,14 @@ jobs: matrix: include: + - os: ubuntu-20.04 + qt_version: 5.12.8 + qt_host: linux + - os: ubuntu-20.04 qt_version: 5.15.2 qt_host: linux + app_image: true - os: windows-2022 qt_version: 5.15.2 @@ -94,15 +99,15 @@ jobs: - name: Install Ninja uses: urkle/action-get-ninja@v1 - - name: Download linuxdeploy family - if: runner.os == 'Linux' + - name: Download linuxdeploy family for AppImage on Linux + if: matrix.app_image == true run: | wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" wget "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage" wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" - name: Download JREs for AppImage on Linux - if: runner.os == 'Linux' + if: matrix.app_image == true shell: bash run: | ${{ github.workspace }}/.github/scripts/prepare_JREs.sh @@ -126,13 +131,13 @@ jobs: run: | cmake --install ${{ env.BUILD_DIR }} - - name: Install for AppImage on Linux + - name: Install on Linux if: runner.os == 'Linux' run: | DESTDIR=${{ env.INSTALL_DIR }} cmake --install ${{ env.BUILD_DIR }} - name: Bundle AppImage - if: runner.os == 'Linux' + if: matrix.app_image == true shell: bash run: | export OUTPUT="PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage" @@ -175,8 +180,21 @@ jobs: cd ${{ env.INSTALL_DIR }} tar -czf ../PolyMC.tar.gz * + - name: tar on Linux + if: runner.os == 'Linux' && matrix.app_image != true + run: | + cd ${{ env.INSTALL_DIR }} + tar -czf ../PolyMC.tar.gz * + + - name: Upload Linux tar.gz + if: runner.os == 'Linux' && matrix.app_image != true + uses: actions/upload-artifact@v2 + with: + name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} + path: PolyMC.tar.gz + - name: Upload AppImage for Linux - if: runner.os == 'Linux' + if: matrix.app_image == true uses: actions/upload-artifact@v2 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 4a162665..3b9cec16 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -56,6 +56,7 @@ jobs: run: | rm -rf *Debug* + mv PolyMC-Linux*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage mv PolyMC-Windows* PolyMC-Windows-${{ env.VERSION }} mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz @@ -64,6 +65,16 @@ jobs: zip -r -9 ../PolyMC-Windows-${{ env.VERSION }}.zip * cd .. + - name: Upload Linux asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_name: PolyMC-Linux-${{ env.VERSION }}.tar.gz + asset_path: PolyMC-Linux-${{ env.VERSION }}.tar.gz + asset_content_type: application/gzip + - name: Upload Linux AppImage asset uses: actions/upload-release-asset@v1 env: From d3e7d30ee0e242a6f591b61295adf05bf8488de1 Mon Sep 17 00:00:00 2001 From: txtsd Date: Mon, 21 Feb 2022 20:56:05 +0530 Subject: [PATCH 019/605] Create releases in a separate workflow --- .github/workflows/trigger_builds.yml | 94 ++----------------------- .github/workflows/trigger_release.yml | 99 +++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 89 deletions(-) create mode 100644 .github/workflows/trigger_release.yml diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 3b9cec16..1dfc728e 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -1,7 +1,11 @@ name: Build Application on: - [push, pull_request, workflow_dispatch] + push: + branches-ignore: + - 'stable' + pull_request: + workflow_dispatch: jobs: @@ -16,91 +20,3 @@ jobs: uses: ./.github/workflows/build.yml with: build_type: Release - - create_release: - if: contains(github.base_ref, 'refs/heads/stable') && startsWith(github.ref, 'refs/tags/') - needs: build_release - runs-on: ubuntu-latest - outputs: - upload_url: ${{ steps.create_release.outputs.upload_url }} - steps: - - name: Grab and store version - run: | - tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") - echo "VERSION=$tag_name" >> $GITHUB_ENV - - name: Create release - id: create_release - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - name: PolyMC ${{ env.VERSION }} - draft: true - prerelease: false - - upload_release: - needs: create_release - runs-on: ubuntu-latest - steps: - - - name: Download artifacts - uses: actions/download-artifact@v2 - - - name: Grab and store version - run: | - tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") - echo "VERSION=$tag_name" >> $GITHUB_ENV - - - name: Package artifacts properly - run: | - rm -rf *Debug* - - mv PolyMC-Linux*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz - mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage - mv PolyMC-Windows* PolyMC-Windows-${{ env.VERSION }} - mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz - - cd PolyMC-Windows-${{ env.VERSION }} - zip -r -9 ../PolyMC-Windows-${{ env.VERSION }}.zip * - cd .. - - - name: Upload Linux asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: PolyMC-Linux-${{ env.VERSION }}.tar.gz - asset_path: PolyMC-Linux-${{ env.VERSION }}.tar.gz - asset_content_type: application/gzip - - - name: Upload Linux AppImage asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage - asset_path: PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage - asset_content_type: application/x-executable - - - name: Upload Windows asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: PolyMC-Windows-${{ env.VERSION }}.zip - asset_path: PolyMC-Windows-${{ env.VERSION }}.zip - asset_content_type: application/zip - - - name: Upload macOS asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: PolyMC-macOS-${{ env.VERSION }}.tar.gz - asset_path: PolyMC-macOS-${{ env.VERSION }}.tar.gz - asset_content_type: application/gzip diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml new file mode 100644 index 00000000..b487e731 --- /dev/null +++ b/.github/workflows/trigger_release.yml @@ -0,0 +1,99 @@ +name: Build Application and Make Release + +on: + push: + tags: + - '*' + +jobs: + + build_release: + name: Build Release + uses: ./.github/workflows/build.yml + with: + build_type: Release + + create_release: + needs: build_release + runs-on: ubuntu-latest + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + steps: + - name: Grab and store version + run: | + tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") + echo "VERSION=$tag_name" >> $GITHUB_ENV + - name: Create release + id: create_release + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + name: PolyMC ${{ env.VERSION }} + draft: true + prerelease: false + + upload_release: + needs: create_release + runs-on: ubuntu-latest + steps: + + - name: Download artifacts + uses: actions/download-artifact@v2 + + - name: Grab and store version + run: | + tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") + echo "VERSION=$tag_name" >> $GITHUB_ENV + + - name: Package artifacts properly + run: | + mv PolyMC-Linux*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz + mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage + mv PolyMC-Windows* PolyMC-Windows-${{ env.VERSION }} + mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz + + cd PolyMC-Windows-${{ env.VERSION }} + zip -r -9 ../PolyMC-Windows-${{ env.VERSION }}.zip * + cd .. + + - name: Upload Linux asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_name: PolyMC-Linux-${{ env.VERSION }}.tar.gz + asset_path: PolyMC-Linux-${{ env.VERSION }}.tar.gz + asset_content_type: application/gzip + + - name: Upload Linux AppImage asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_name: PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage + asset_path: PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage + asset_content_type: application/x-executable + + - name: Upload Windows asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_name: PolyMC-Windows-${{ env.VERSION }}.zip + asset_path: PolyMC-Windows-${{ env.VERSION }}.zip + asset_content_type: application/zip + + - name: Upload macOS asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_name: PolyMC-macOS-${{ env.VERSION }}.tar.gz + asset_path: PolyMC-macOS-${{ env.VERSION }}.tar.gz + asset_content_type: application/gzip From 6d1f9d4d02cef93e92ebf0ba46a1c1296da50673 Mon Sep 17 00:00:00 2001 From: swirl Date: Mon, 21 Feb 2022 12:44:34 -0500 Subject: [PATCH 020/605] fix --- launcher/ui/pages/global/LauncherPage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 3722072d..6f68d0dd 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -38,7 +38,7 @@ QTabWidget::Rounded - 1 + 0 From 3059f130114a542f9a28ac7b665cc844ee90b03d Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 21 Feb 2022 22:11:10 +0100 Subject: [PATCH 021/605] refactor: drop migration for pre-component instances --- launcher/minecraft/MinecraftInstance.cpp | 11 - launcher/minecraft/PackProfile.cpp | 254 ----------------------- launcher/minecraft/PackProfile.h | 2 - launcher/minecraft/PackProfile_p.h | 12 -- 4 files changed, 279 deletions(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 7327f9d5..6db12c42 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -124,18 +124,7 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO m_settings->registerSetting("JoinServerOnLaunch", false); m_settings->registerSetting("JoinServerOnLaunchAddress", ""); - // DEPRECATED: Read what versions the user configuration thinks should be used - m_settings->registerSetting({"IntendedVersion", "MinecraftVersion"}, ""); - m_settings->registerSetting("LWJGLVersion", ""); - m_settings->registerSetting("ForgeVersion", ""); - m_settings->registerSetting("LiteloaderVersion", ""); - m_components.reset(new PackProfile(this)); - m_components->setOldConfigVersion("net.minecraft", m_settings->get("IntendedVersion").toString()); - auto setting = m_settings->getSetting("LWJGLVersion"); - m_components->setOldConfigVersion("org.lwjgl", m_settings->get("LWJGLVersion").toString()); - m_components->setOldConfigVersion("net.minecraftforge", m_settings->get("ForgeVersion").toString()); - m_components->setOldConfigVersion("com.mumfrey.liteloader", m_settings->get("LiteloaderVersion").toString()); } void MinecraftInstance::saveNow() diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 59a8f133..d516e555 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -272,18 +272,6 @@ void PackProfile::save_internal() bool PackProfile::load() { auto filename = componentsFilePath(); - QFile componentsFile(filename); - - // migrate old config to new one, if needed - if(!componentsFile.exists()) - { - if(!migratePreComponentConfig()) - { - // FIXME: the user should be notified... - qCritical() << "Failed to convert old pre-component config for instance" << d->m_instance->name(); - return false; - } - } // load the new component list and swap it with the current one... ComponentContainer newComponents; @@ -369,239 +357,6 @@ void PackProfile::updateFailed(const QString& error) invalidateLaunchProfile(); } -// NOTE this is really old stuff, and only needs to be used when loading the old hardcoded component-unaware format (loadPreComponentConfig). -static void upgradeDeprecatedFiles(QString root, QString instanceName) -{ - auto versionJsonPath = FS::PathCombine(root, "version.json"); - auto customJsonPath = FS::PathCombine(root, "custom.json"); - auto mcJson = FS::PathCombine(root, "patches" , "net.minecraft.json"); - - QString sourceFile; - QString renameFile; - - // convert old crap. - if(QFile::exists(customJsonPath)) - { - sourceFile = customJsonPath; - renameFile = versionJsonPath; - } - else if(QFile::exists(versionJsonPath)) - { - sourceFile = versionJsonPath; - } - if(!sourceFile.isEmpty() && !QFile::exists(mcJson)) - { - if(!FS::ensureFilePathExists(mcJson)) - { - qWarning() << "Couldn't create patches folder for" << instanceName; - return; - } - if(!renameFile.isEmpty() && QFile::exists(renameFile)) - { - if(!QFile::rename(renameFile, renameFile + ".old")) - { - qWarning() << "Couldn't rename" << renameFile << "to" << renameFile + ".old" << "in" << instanceName; - return; - } - } - auto file = ProfileUtils::parseJsonFile(QFileInfo(sourceFile), false); - ProfileUtils::removeLwjglFromPatch(file); - file->uid = "net.minecraft"; - file->version = file->minecraftVersion; - file->name = "Minecraft"; - - Meta::Require needsLwjgl; - needsLwjgl.uid = "org.lwjgl"; - file->requires.insert(needsLwjgl); - - if(!ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), mcJson)) - { - return; - } - if(!QFile::rename(sourceFile, sourceFile + ".old")) - { - qWarning() << "Couldn't rename" << sourceFile << "to" << sourceFile + ".old" << "in" << instanceName; - return; - } - } -} - -/* - * Migrate old layout to the component based one... - * - Part of the version information is taken from `instance.cfg` (fed to this class from outside). - * - Part is taken from the old order.json file. - * - Part is loaded from loose json files in the instance's `patches` directory. - */ -bool PackProfile::migratePreComponentConfig() -{ - // upgrade the very old files from the beginnings of MultiMC 5 - upgradeDeprecatedFiles(d->m_instance->instanceRoot(), d->m_instance->name()); - - QList components; - QSet loaded; - - auto addBuiltinPatch = [&](const QString &uid, bool asDependency, const QString & emptyVersion, const Meta::Require & req, const Meta::Require & conflict) - { - auto jsonFilePath = FS::PathCombine(d->m_instance->instanceRoot(), "patches" , uid + ".json"); - auto intendedVersion = d->getOldConfigVersion(uid); - // load up the base minecraft patch - ComponentPtr component; - if(QFile::exists(jsonFilePath)) - { - if(intendedVersion.isEmpty()) - { - intendedVersion = emptyVersion; - } - auto file = ProfileUtils::parseJsonFile(QFileInfo(jsonFilePath), false); - // fix uid - file->uid = uid; - // if version is missing, add it from the outside. - if(file->version.isEmpty()) - { - file->version = intendedVersion; - } - // if this is a dependency (LWJGL), mark it also as volatile - if(asDependency) - { - file->m_volatile = true; - } - // insert requirements if needed - if(!req.uid.isEmpty()) - { - file->requires.insert(req); - } - // insert conflicts if needed - if(!conflict.uid.isEmpty()) - { - file->conflicts.insert(conflict); - } - // FIXME: @QUALITY do not ignore return value - ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), jsonFilePath); - component = new Component(this, uid, file); - component->m_version = intendedVersion; - } - else if(!intendedVersion.isEmpty()) - { - auto metaVersion = APPLICATION->metadataIndex()->get(uid, intendedVersion); - component = new Component(this, metaVersion); - } - else - { - return; - } - component->m_dependencyOnly = asDependency; - component->m_important = !asDependency; - components.append(component); - }; - // TODO: insert depends and conflicts here if these are customized files... - Meta::Require reqLwjgl; - reqLwjgl.uid = "org.lwjgl"; - reqLwjgl.suggests = "2.9.1"; - Meta::Require conflictLwjgl3; - conflictLwjgl3.uid = "org.lwjgl3"; - Meta::Require nullReq; - addBuiltinPatch("org.lwjgl", true, "2.9.1", nullReq, conflictLwjgl3); - addBuiltinPatch("net.minecraft", false, QString(), reqLwjgl, nullReq); - - // first, collect all other file-based patches and load them - QMap loadedComponents; - QDir patchesDir(FS::PathCombine(d->m_instance->instanceRoot(),"patches")); - for (auto info : patchesDir.entryInfoList(QStringList() << "*.json", QDir::Files)) - { - // parse the file - qDebug() << "Reading" << info.fileName(); - auto file = ProfileUtils::parseJsonFile(info, true); - - // correct missing or wrong uid based on the file name - QString uid = info.completeBaseName(); - - // ignore builtins, they've been handled already - if (uid == "net.minecraft") - continue; - if (uid == "org.lwjgl") - continue; - - // handle horrible corner cases - if(uid.isEmpty()) - { - // if you have a file named '.json', make it just go away. - // FIXME: @QUALITY do not ignore return value - QFile::remove(info.absoluteFilePath()); - continue; - } - file->uid = uid; - // FIXME: @QUALITY do not ignore return value - ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), info.absoluteFilePath()); - - auto component = new Component(this, file->uid, file); - auto version = d->getOldConfigVersion(file->uid); - if(!version.isEmpty()) - { - component->m_version = version; - } - loadedComponents[file->uid] = component; - } - // try to load the other 'hardcoded' patches (forge, liteloader), if they weren't loaded from files - auto loadSpecial = [&](const QString & uid, int order) - { - auto patchVersion = d->getOldConfigVersion(uid); - if(!patchVersion.isEmpty() && !loadedComponents.contains(uid)) - { - auto patch = new Component(this, APPLICATION->metadataIndex()->get(uid, patchVersion)); - patch->setOrder(order); - loadedComponents[uid] = patch; - } - }; - loadSpecial("net.minecraftforge", 5); - loadSpecial("com.mumfrey.liteloader", 10); - - // load the old order.json file, if present - ProfileUtils::PatchOrder userOrder; - ProfileUtils::readOverrideOrders(FS::PathCombine(d->m_instance->instanceRoot(), "order.json"), userOrder); - - // now add all the patches by user sort order - for (auto uid : userOrder) - { - // ignore builtins - if (uid == "net.minecraft") - continue; - if (uid == "org.lwjgl") - continue; - // ordering has a patch that is gone? - if(!loadedComponents.contains(uid)) - { - continue; - } - components.append(loadedComponents.take(uid)); - } - - // is there anything left to sort? - this is used when there are leftover components that aren't part of the order.json - if(!loadedComponents.isEmpty()) - { - // inserting into multimap by order number as key sorts the patches and detects duplicates - QMultiMap files; - auto iter = loadedComponents.begin(); - while(iter != loadedComponents.end()) - { - files.insert((*iter)->getOrder(), *iter); - iter++; - } - - // then just extract the patches and put them in the list - for (auto order : files.keys()) - { - const auto &values = files.values(order); - for(auto &value: values) - { - // TODO: put back the insertion of problem messages here, so the user knows about the id duplication - components.append(value); - } - } - } - // new we have a complete list of components... - return savePackProfile(componentsFilePath(), components); -} - // END: save/load void PackProfile::appendComponent(ComponentPtr component) @@ -1169,15 +924,6 @@ std::shared_ptr PackProfile::getProfile() const return d->m_profile; } -void PackProfile::setOldConfigVersion(const QString& uid, const QString& version) -{ - if(version.isEmpty()) - { - return; - } - d->m_oldConfigVersions[uid] = version; -} - bool PackProfile::setComponentVersion(const QString& uid, const QString& version, bool important) { auto iter = d->componentIndex.find(uid); diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index f30deb5a..989d1c6a 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -143,8 +143,6 @@ private: bool installCustomJar_internal(QString filepath); bool removeComponent_internal(ComponentPtr patch); - bool migratePreComponentConfig(); - private: /* data */ std::unique_ptr d; diff --git a/launcher/minecraft/PackProfile_p.h b/launcher/minecraft/PackProfile_p.h index fce921bb..715e0460 100644 --- a/launcher/minecraft/PackProfile_p.h +++ b/launcher/minecraft/PackProfile_p.h @@ -18,18 +18,6 @@ struct PackProfileData // the launch profile (volatile, temporary thing created on demand) std::shared_ptr m_profile; - // version information migrated from instance.cfg file. Single use on migration! - std::map m_oldConfigVersions; - QString getOldConfigVersion(const QString& uid) const - { - const auto iter = m_oldConfigVersions.find(uid); - if(iter != m_oldConfigVersions.cend()) - { - return (*iter).second; - } - return QString(); - } - // persistent list of components and related machinery ComponentContainer components; ComponentIndex componentIndex; From a70d1f1a9184a9f0506c439be1009e5ba3c77b5b Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 21 Feb 2022 22:30:44 +0100 Subject: [PATCH 022/605] refactor: drop LegacyInstance --- launcher/CMakeLists.txt | 10 - launcher/InstanceList.cpp | 5 - launcher/InstancePageProvider.h | 41 +-- launcher/minecraft/legacy/LegacyInstance.cpp | 256 ------------------ launcher/minecraft/legacy/LegacyInstance.h | 142 ---------- launcher/minecraft/legacy/LegacyModList.cpp | 136 ---------- launcher/minecraft/legacy/LegacyModList.h | 47 ---- .../minecraft/legacy/LegacyUpgradeTask.cpp | 138 ---------- launcher/minecraft/legacy/LegacyUpgradeTask.h | 29 -- .../ui/pages/instance/LegacyUpgradePage.cpp | 51 ---- .../ui/pages/instance/LegacyUpgradePage.h | 64 ----- .../ui/pages/instance/LegacyUpgradePage.ui | 54 ---- 12 files changed, 14 insertions(+), 959 deletions(-) delete mode 100644 launcher/minecraft/legacy/LegacyInstance.cpp delete mode 100644 launcher/minecraft/legacy/LegacyInstance.h delete mode 100644 launcher/minecraft/legacy/LegacyModList.cpp delete mode 100644 launcher/minecraft/legacy/LegacyModList.h delete mode 100644 launcher/minecraft/legacy/LegacyUpgradeTask.cpp delete mode 100644 launcher/minecraft/legacy/LegacyUpgradeTask.h delete mode 100644 launcher/ui/pages/instance/LegacyUpgradePage.cpp delete mode 100644 launcher/ui/pages/instance/LegacyUpgradePage.h delete mode 100644 launcher/ui/pages/instance/LegacyUpgradePage.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 90149c3b..86c05651 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -286,13 +286,6 @@ set(MINECRAFT_SOURCES minecraft/launch/VerifyJavaInstall.cpp minecraft/launch/VerifyJavaInstall.h - minecraft/legacy/LegacyModList.h - minecraft/legacy/LegacyModList.cpp - minecraft/legacy/LegacyInstance.h - minecraft/legacy/LegacyInstance.cpp - minecraft/legacy/LegacyUpgradeTask.h - minecraft/legacy/LegacyUpgradeTask.cpp - minecraft/GradleSpecifier.h minecraft/MinecraftInstance.cpp minecraft/MinecraftInstance.h @@ -701,8 +694,6 @@ SET(LAUNCHER_SOURCES ui/pages/instance/OtherLogsPage.h ui/pages/instance/ServersPage.cpp ui/pages/instance/ServersPage.h - ui/pages/instance/LegacyUpgradePage.cpp - ui/pages/instance/LegacyUpgradePage.h ui/pages/instance/WorldListPage.cpp ui/pages/instance/WorldListPage.h @@ -884,7 +875,6 @@ qt5_wrap_ui(LAUNCHER_UI ui/pages/instance/InstanceSettingsPage.ui ui/pages/instance/VersionPage.ui ui/pages/instance/WorldListPage.ui - ui/pages/instance/LegacyUpgradePage.ui ui/pages/instance/ScreenshotsPage.ui ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui ui/pages/modplatform/atlauncher/AtlPage.ui diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index ad18740b..5637983b 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -32,7 +32,6 @@ #include "BaseInstance.h" #include "InstanceTask.h" #include "settings/INISettingsObject.h" -#include "minecraft/legacy/LegacyInstance.h" #include "NullInstance.h" #include "minecraft/MinecraftInstance.h" #include "FileSystem.h" @@ -553,10 +552,6 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) { inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); } - else if (inst_type == "Legacy") - { - inst.reset(new LegacyInstance(m_globalSettings, instanceSettings, instanceRoot)); - } else { inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot)); diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 97eeab8c..357157d0 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -1,6 +1,5 @@ #pragma once #include "minecraft/MinecraftInstance.h" -#include "minecraft/legacy/LegacyInstance.h" #include #include "ui/pages/BasePage.h" #include "ui/pages/BasePageProvider.h" @@ -14,7 +13,6 @@ #include "ui/pages/instance/ScreenshotsPage.h" #include "ui/pages/instance/InstanceSettingsPage.h" #include "ui/pages/instance/OtherLogsPage.h" -#include "ui/pages/instance/LegacyUpgradePage.h" #include "ui/pages/instance/WorldListPage.h" #include "ui/pages/instance/ServersPage.h" #include "ui/pages/instance/GameOptionsPage.h" @@ -34,31 +32,20 @@ public: QList values; values.append(new LogPage(inst)); std::shared_ptr onesix = std::dynamic_pointer_cast(inst); - if(onesix) - { - values.append(new VersionPage(onesix.get())); - auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList(), "mods", "loadermods", tr("Mods"), "Loader-mods"); - modsPage->setFilter("%1 (*.zip *.jar *.litemod)"); - values.append(modsPage); - values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList(), "coremods", "coremods", tr("Core mods"), "Core-mods")); - values.append(new ResourcePackPage(onesix.get())); - values.append(new TexturePackPage(onesix.get())); - values.append(new ShaderPackPage(onesix.get())); - values.append(new NotesPage(onesix.get())); - values.append(new WorldListPage(onesix.get(), 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())); - } - std::shared_ptr legacy = std::dynamic_pointer_cast(inst); - if(legacy) - { - values.append(new LegacyUpgradePage(legacy)); - values.append(new NotesPage(legacy.get())); - values.append(new WorldListPage(legacy.get(), legacy->worldList())); - values.append(new ScreenshotsPage(FS::PathCombine(legacy->gameRoot(), "screenshots"))); - } + values.append(new VersionPage(onesix.get())); + auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList(), "mods", "loadermods", tr("Mods"), "Loader-mods"); + modsPage->setFilter("%1 (*.zip *.jar *.litemod)"); + values.append(modsPage); + values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList(), "coremods", "coremods", tr("Core mods"), "Core-mods")); + values.append(new ResourcePackPage(onesix.get())); + values.append(new TexturePackPage(onesix.get())); + values.append(new ShaderPackPage(onesix.get())); + values.append(new NotesPage(onesix.get())); + values.append(new WorldListPage(onesix.get(), 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) { diff --git a/launcher/minecraft/legacy/LegacyInstance.cpp b/launcher/minecraft/legacy/LegacyInstance.cpp deleted file mode 100644 index f467ec06..00000000 --- a/launcher/minecraft/legacy/LegacyInstance.cpp +++ /dev/null @@ -1,256 +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 -#include - -#include "LegacyInstance.h" - -#include "minecraft/legacy/LegacyModList.h" -#include "minecraft/WorldList.h" -#include -#include - -LegacyInstance::LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) - : BaseInstance(globalSettings, settings, rootDir) -{ - settings->registerSetting("NeedsRebuild", true); - settings->registerSetting("ShouldUpdate", false); - settings->registerSetting("JarVersion", QString()); - settings->registerSetting("IntendedJarVersion", QString()); - /* - * custom base jar has no default. it is determined in code... see the accessor methods for - *it - * - * for instances that DO NOT have the CustomBaseJar setting (legacy instances), - * [.]minecraft/bin/mcbackup.jar is the default base jar - */ - settings->registerSetting("UseCustomBaseJar", true); - settings->registerSetting("CustomBaseJar", ""); -} - -QString LegacyInstance::mainJarToPreserve() const -{ - bool customJar = m_settings->get("UseCustomBaseJar").toBool(); - if(customJar) - { - auto base = baseJar(); - if(QFile::exists(base)) - { - return base; - } - } - auto runnable = runnableJar(); - if(QFile::exists(runnable)) - { - return runnable; - } - return QString(); -} - - -QString LegacyInstance::baseJar() const -{ - bool customJar = m_settings->get("UseCustomBaseJar").toBool(); - if (customJar) - { - return customBaseJar(); - } - else - return defaultBaseJar(); -} - -QString LegacyInstance::customBaseJar() const -{ - QString value = m_settings->get("CustomBaseJar").toString(); - if (value.isNull() || value.isEmpty()) - { - return defaultCustomBaseJar(); - } - return value; -} - -bool LegacyInstance::shouldUseCustomBaseJar() const -{ - return m_settings->get("UseCustomBaseJar").toBool(); -} - - -Task::Ptr LegacyInstance::createUpdateTask(Net::Mode) -{ - return nullptr; -} - -std::shared_ptr LegacyInstance::jarModList() const -{ - if (!jar_mod_list) - { - auto list = new LegacyModList(jarModsDir(), modListFile()); - jar_mod_list.reset(list); - } - jar_mod_list->update(); - return jar_mod_list; -} - -QString LegacyInstance::gameRoot() const -{ - QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); - QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft")); - - if (mcDir.exists() && !dotMCDir.exists()) - return mcDir.filePath(); - else - return dotMCDir.filePath(); -} - -QString LegacyInstance::binRoot() const -{ - return FS::PathCombine(gameRoot(), "bin"); -} - -QString LegacyInstance::modsRoot() const { - return FS::PathCombine(gameRoot(), "mods"); -} - - -QString LegacyInstance::jarModsDir() const -{ - return FS::PathCombine(instanceRoot(), "instMods"); -} - -QString LegacyInstance::libDir() const -{ - return FS::PathCombine(gameRoot(), "lib"); -} - -QString LegacyInstance::savesDir() const -{ - return FS::PathCombine(gameRoot(), "saves"); -} - -QString LegacyInstance::coreModsDir() const -{ - return FS::PathCombine(gameRoot(), "coremods"); -} - -QString LegacyInstance::resourceDir() const -{ - return FS::PathCombine(gameRoot(), "resources"); -} -QString LegacyInstance::texturePacksDir() const -{ - return FS::PathCombine(gameRoot(), "texturepacks"); -} - -QString LegacyInstance::runnableJar() const -{ - return FS::PathCombine(binRoot(), "minecraft.jar"); -} - -QString LegacyInstance::modListFile() const -{ - return FS::PathCombine(instanceRoot(), "modlist"); -} - -QString LegacyInstance::instanceConfigFolder() const -{ - return FS::PathCombine(gameRoot(), "config"); -} - -bool LegacyInstance::shouldRebuild() const -{ - return m_settings->get("NeedsRebuild").toBool(); -} - -QString LegacyInstance::currentVersionId() const -{ - return m_settings->get("JarVersion").toString(); -} - -QString LegacyInstance::intendedVersionId() const -{ - return m_settings->get("IntendedJarVersion").toString(); -} - -bool LegacyInstance::shouldUpdate() const -{ - QVariant var = settings()->get("ShouldUpdate"); - if (!var.isValid() || var.toBool() == false) - { - return intendedVersionId() != currentVersionId(); - } - return true; -} - -QString LegacyInstance::defaultBaseJar() const -{ - return "versions/" + intendedVersionId() + "/" + intendedVersionId() + ".jar"; -} - -QString LegacyInstance::defaultCustomBaseJar() const -{ - return FS::PathCombine(binRoot(), "mcbackup.jar"); -} - -std::shared_ptr LegacyInstance::worldList() const -{ - if (!m_world_list) - { - m_world_list.reset(new WorldList(savesDir())); - } - return m_world_list; -} - -QString LegacyInstance::typeName() const -{ - return tr("Legacy"); -} - -QString LegacyInstance::getStatusbarDescription() -{ - return tr("Instance from previous versions."); -} - -QStringList LegacyInstance::verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) -{ - QStringList out; - - auto alltraits = traits(); - if(alltraits.size()) - { - out << "Traits:"; - for (auto trait : alltraits) - { - out << " " + trait; - } - out << ""; - } - - QString windowParams; - if (settings()->get("LaunchMaximized").toBool()) - { - out << "Window size: max (if available)"; - } - else - { - auto width = settings()->get("MinecraftWinWidth").toInt(); - auto height = settings()->get("MinecraftWinHeight").toInt(); - out << "Window size: " + QString::number(width) + " x " + QString::number(height); - } - out << ""; - return out; -} diff --git a/launcher/minecraft/legacy/LegacyInstance.h b/launcher/minecraft/legacy/LegacyInstance.h deleted file mode 100644 index 298543f7..00000000 --- a/launcher/minecraft/legacy/LegacyInstance.h +++ /dev/null @@ -1,142 +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 "BaseInstance.h" -#include "launch/LaunchTask.h" - -class ModFolderModel; -class LegacyModList; -class WorldList; -class Task; -/* - * WHY: Legacy instances - from MultiMC 3 and 4 - are here only to provide a way to upgrade them to the current format. - */ -class LegacyInstance : public BaseInstance -{ - Q_OBJECT -public: - - explicit LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); - - virtual void saveNow() override {} - - /// Path to the instance's minecraft.jar - QString runnableJar() const; - - //! Path to the instance's modlist file. - QString modListFile() const; - - ////// Directories ////// - QString libDir() const; - QString savesDir() const; - QString texturePacksDir() const; - QString jarModsDir() const; - QString coreModsDir() const; - QString resourceDir() const; - - QString instanceConfigFolder() const override; - - QString gameRoot() const override; // Path to the instance's minecraft directory. - QString modsRoot() const override; // Path to the instance's minecraft directory. - QString binRoot() const; // Path to the instance's minecraft bin directory. - - /// Get the curent base jar of this instance. By default, it's the - /// versions/$version/$version.jar - QString baseJar() const; - - /// the default base jar of this instance - QString defaultBaseJar() const; - /// the default custom base jar of this instance - QString defaultCustomBaseJar() const; - - // the main jar that we actually want to keep when migrating the instance - QString mainJarToPreserve() const; - - /*! - * Whether or not custom base jar is used - */ - bool shouldUseCustomBaseJar() const; - - /*! - * The value of the custom base jar - */ - QString customBaseJar() const; - - std::shared_ptr jarModList() const; - std::shared_ptr worldList() const; - - /*! - * Whether or not the instance's minecraft.jar needs to be rebuilt. - * If this is true, when the instance launches, its jar mods will be - * re-added to a fresh minecraft.jar file. - */ - bool shouldRebuild() const; - - QString currentVersionId() const; - QString intendedVersionId() const; - - QSet traits() const override - { - return {"legacy-instance", "texturepacks"}; - }; - - virtual bool shouldUpdate() const; - virtual Task::Ptr createUpdateTask(Net::Mode mode) override; - - virtual QString typeName() const override; - - bool canLaunch() const override - { - return false; - } - bool canEdit() const override - { - return true; - } - bool canExport() const override - { - return false; - } - shared_qobject_ptr createLaunchTask( - AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override - { - return nullptr; - } - IPathMatcher::Ptr getLogFileMatcher() override - { - return nullptr; - } - QString getLogFileRoot() override - { - return gameRoot(); - } - - QString getStatusbarDescription() override; - QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override; - - QProcessEnvironment createEnvironment() override - { - return QProcessEnvironment(); - } - QMap getVariables() const override - { - return {}; - } -protected: - mutable std::shared_ptr jar_mod_list; - mutable std::shared_ptr m_world_list; -}; diff --git a/launcher/minecraft/legacy/LegacyModList.cpp b/launcher/minecraft/legacy/LegacyModList.cpp deleted file mode 100644 index e9948ab1..00000000 --- a/launcher/minecraft/legacy/LegacyModList.cpp +++ /dev/null @@ -1,136 +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 "LegacyModList.h" -#include -#include -#include - -LegacyModList::LegacyModList(const QString &dir, const QString &list_file) - : m_dir(dir), m_list_file(list_file) -{ - FS::ensureFolderPathExists(m_dir.absolutePath()); - m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); - m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); -} - - struct OrderItem - { - QString id; - bool enabled = false; - }; - typedef QList OrderList; - -static void internalSort(QList &what) -{ - auto predicate = [](const LegacyModList::Mod &left, const LegacyModList::Mod &right) - { - return left.fileName().localeAwareCompare(right.fileName()) < 0; - }; - std::sort(what.begin(), what.end(), predicate); -} - -static OrderList readListFile(const QString &m_list_file) -{ - OrderList itemList; - if (m_list_file.isNull() || m_list_file.isEmpty()) - return itemList; - - QFile textFile(m_list_file); - if (!textFile.open(QIODevice::ReadOnly | QIODevice::Text)) - return OrderList(); - - QTextStream textStream; - textStream.setAutoDetectUnicode(true); - textStream.setDevice(&textFile); - while (true) - { - QString line = textStream.readLine(); - if (line.isNull() || line.isEmpty()) - break; - else - { - OrderItem it; - it.enabled = !line.endsWith(".disabled"); - if (!it.enabled) - { - line.chop(9); - } - it.id = line; - itemList.append(it); - } - } - textFile.close(); - return itemList; -} - -bool LegacyModList::update() -{ - if (!m_dir.exists() || !m_dir.isReadable()) - return false; - - QList orderedMods; - QList newMods; - m_dir.refresh(); - auto folderContents = m_dir.entryInfoList(); - - // first, process the ordered items (if any) - OrderList listOrder = readListFile(m_list_file); - for (auto item : listOrder) - { - QFileInfo infoEnabled(m_dir.filePath(item.id)); - QFileInfo infoDisabled(m_dir.filePath(item.id + ".disabled")); - int idxEnabled = folderContents.indexOf(infoEnabled); - int idxDisabled = folderContents.indexOf(infoDisabled); - bool isEnabled; - // if both enabled and disabled versions are present, it's a special case... - if (idxEnabled >= 0 && idxDisabled >= 0) - { - // we only process the one we actually have in the order file. - // and exactly as we have it. - // THIS IS A CORNER CASE - isEnabled = item.enabled; - } - else - { - // only one is present. - // we pick the one that we found. - // we assume the mod was enabled/disabled by external means - isEnabled = idxEnabled >= 0; - } - int idx = isEnabled ? idxEnabled : idxDisabled; - QFileInfo &info = isEnabled ? infoEnabled : infoDisabled; - // if the file from the index file exists - if (idx != -1) - { - // remove from the actual folder contents list - folderContents.takeAt(idx); - // append the new mod - orderedMods.append(info); - } - } - // if there are any untracked files... append them sorted at the end - if (folderContents.size()) - { - for (auto entry : folderContents) - { - newMods.append(entry); - } - internalSort(newMods); - orderedMods.append(newMods); - } - mods.swap(orderedMods); - return true; -} diff --git a/launcher/minecraft/legacy/LegacyModList.h b/launcher/minecraft/legacy/LegacyModList.h deleted file mode 100644 index fade736e..00000000 --- a/launcher/minecraft/legacy/LegacyModList.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 -#include -#include - -class LegacyModList -{ -public: - - using Mod = QFileInfo; - - LegacyModList(const QString &dir, const QString &list_file = QString()); - - /// Reloads the mod list and returns true if the list changed. - bool update(); - - QDir dir() - { - return m_dir; - } - - const QList & allMods() - { - return mods; - } - -protected: - QDir m_dir; - QString m_list_file; - QList mods; -}; diff --git a/launcher/minecraft/legacy/LegacyUpgradeTask.cpp b/launcher/minecraft/legacy/LegacyUpgradeTask.cpp deleted file mode 100644 index a4ea60cd..00000000 --- a/launcher/minecraft/legacy/LegacyUpgradeTask.cpp +++ /dev/null @@ -1,138 +0,0 @@ -#include "LegacyUpgradeTask.h" -#include "settings/INISettingsObject.h" -#include "FileSystem.h" -#include "NullInstance.h" -#include "pathmatcher/RegexpMatcher.h" -#include -#include "LegacyInstance.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" -#include "LegacyModList.h" -#include "classparser.h" - -LegacyUpgradeTask::LegacyUpgradeTask(InstancePtr origInstance) -{ - m_origInstance = origInstance; -} - -void LegacyUpgradeTask::executeTask() -{ - setStatus(tr("Copying instance %1").arg(m_origInstance->name())); - - FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); - folderCopy.followSymlinks(true); - - m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), folderCopy); - connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &LegacyUpgradeTask::copyFinished); - connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &LegacyUpgradeTask::copyAborted); - m_copyFutureWatcher.setFuture(m_copyFuture); -} - -static QString decideVersion(const QString& currentVersion, const QString& intendedVersion) -{ - if(intendedVersion != currentVersion) - { - if(!intendedVersion.isEmpty()) - { - return intendedVersion; - } - else if(!currentVersion.isEmpty()) - { - return currentVersion; - } - } - else - { - if(!intendedVersion.isEmpty()) - { - return intendedVersion; - } - } - return QString(); -} - -void LegacyUpgradeTask::copyFinished() -{ - auto successful = m_copyFuture.result(); - if(!successful) - { - emitFailed(tr("Instance folder copy failed.")); - return; - } - auto legacyInst = std::dynamic_pointer_cast(m_origInstance); - - auto instanceSettings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); - instanceSettings->registerSetting("InstanceType", "Legacy"); - instanceSettings->set("InstanceType", "OneSix"); - // NOTE: this scope ensures the instance is fully saved before we emitSucceeded - { - MinecraftInstance inst(m_globalSettings, instanceSettings, m_stagingPath); - inst.setName(m_instName); - - QString preferredVersionNumber = decideVersion(legacyInst->currentVersionId(), legacyInst->intendedVersionId()); - if(preferredVersionNumber.isNull()) - { - // try to decide version based on the jar(s?) - preferredVersionNumber = classparser::GetMinecraftJarVersion(legacyInst->baseJar()); - if(preferredVersionNumber.isNull()) - { - preferredVersionNumber = classparser::GetMinecraftJarVersion(legacyInst->runnableJar()); - if(preferredVersionNumber.isNull()) - { - emitFailed(tr("Could not decide Minecraft version.")); - return; - } - } - } - auto components = inst.getPackProfile(); - components->buildingFromScratch(); - components->setComponentVersion("net.minecraft", preferredVersionNumber, true); - - QString jarPath = legacyInst->mainJarToPreserve(); - if(!jarPath.isNull()) - { - qDebug() << "Preserving base jar! : " << jarPath; - // FIXME: handle case when the jar is unreadable? - // TODO: check the hash, if it's the same as the upstream jar, do not do this - components->installCustomJar(jarPath); - } - - auto jarMods = legacyInst->jarModList()->allMods(); - for(auto & jarMod: jarMods) - { - QString modPath = jarMod.absoluteFilePath(); - qDebug() << "jarMod: " << modPath; - components->installJarMods({modPath}); - } - - // remove all the extra garbage we no longer need - auto removeAll = [&](const QString &root, const QStringList &things) - { - for(auto &thing : things) - { - auto removePath = FS::PathCombine(root, thing); - QFileInfo stat(removePath); - if(stat.isDir()) - { - FS::deletePath(removePath); - } - else - { - QFile::remove(removePath); - } - } - }; - QStringList rootRemovables = {"modlist", "version", "instMods"}; - QStringList mcRemovables = {"bin", "MultiMCLauncher.jar", "icon.png"}; - removeAll(inst.instanceRoot(), rootRemovables); - removeAll(inst.gameRoot(), mcRemovables); - } - emitSucceeded(); -} - -void LegacyUpgradeTask::copyAborted() -{ - emitFailed(tr("Instance folder copy has been aborted.")); - return; -} - diff --git a/launcher/minecraft/legacy/LegacyUpgradeTask.h b/launcher/minecraft/legacy/LegacyUpgradeTask.h deleted file mode 100644 index 542e17b8..00000000 --- a/launcher/minecraft/legacy/LegacyUpgradeTask.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include "InstanceTask.h" -#include "net/NetJob.h" -#include -#include -#include -#include "settings/SettingsObject.h" -#include "BaseVersion.h" -#include "BaseInstance.h" - - -class LegacyUpgradeTask : public InstanceTask -{ - Q_OBJECT -public: - explicit LegacyUpgradeTask(InstancePtr origInstance); - -protected: - //! Entry point for tasks. - virtual void executeTask() override; - void copyFinished(); - void copyAborted(); - -private: /* data */ - InstancePtr m_origInstance; - QFuture m_copyFuture; - QFutureWatcher m_copyFutureWatcher; -}; diff --git a/launcher/ui/pages/instance/LegacyUpgradePage.cpp b/launcher/ui/pages/instance/LegacyUpgradePage.cpp deleted file mode 100644 index cb78af02..00000000 --- a/launcher/ui/pages/instance/LegacyUpgradePage.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include "LegacyUpgradePage.h" -#include "ui_LegacyUpgradePage.h" - -#include "InstanceList.h" -#include "minecraft/legacy/LegacyInstance.h" -#include "minecraft/legacy/LegacyUpgradeTask.h" -#include "Application.h" - -#include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/ProgressDialog.h" - -LegacyUpgradePage::LegacyUpgradePage(InstancePtr inst, QWidget *parent) - : QWidget(parent), ui(new Ui::LegacyUpgradePage), m_inst(inst) -{ - ui->setupUi(this); -} - -LegacyUpgradePage::~LegacyUpgradePage() -{ - delete ui; -} - -void LegacyUpgradePage::runModalTask(Task *task) -{ - connect(task, &Task::failed, [this](QString reason) - { - CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Warning)->show(); - }); - ProgressDialog loadDialog(this); - loadDialog.setSkipButton(true, tr("Abort")); - if(loadDialog.execWithTask(task) == QDialog::Accepted) - { - m_container->requestClose(); - } -} - -void LegacyUpgradePage::on_upgradeButton_clicked() -{ - QString newName = tr("%1 (Migrated)").arg(m_inst->name()); - auto upgradeTask = new LegacyUpgradeTask(m_inst); - upgradeTask->setName(newName); - upgradeTask->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); - upgradeTask->setIcon(m_inst->iconKey()); - unique_qobject_ptr task(APPLICATION->instances()->wrapInstanceTask(upgradeTask)); - runModalTask(task.get()); -} - -bool LegacyUpgradePage::shouldDisplay() const -{ - return !m_inst->isRunning(); -} diff --git a/launcher/ui/pages/instance/LegacyUpgradePage.h b/launcher/ui/pages/instance/LegacyUpgradePage.h deleted file mode 100644 index 7c51956b..00000000 --- a/launcher/ui/pages/instance/LegacyUpgradePage.h +++ /dev/null @@ -1,64 +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 - -#include "minecraft/legacy/LegacyInstance.h" -#include "ui/pages/BasePage.h" -#include -#include "tasks/Task.h" - -namespace Ui -{ -class LegacyUpgradePage; -} - -class LegacyUpgradePage : public QWidget, public BasePage -{ - Q_OBJECT - -public: - explicit LegacyUpgradePage(InstancePtr inst, QWidget *parent = 0); - virtual ~LegacyUpgradePage(); - virtual QString displayName() const override - { - return tr("Upgrade"); - } - virtual QIcon icon() const override - { - return APPLICATION->getThemedIcon("checkupdate"); - } - virtual QString id() const override - { - return "upgrade"; - } - virtual QString helpPage() const override - { - return "Legacy-upgrade"; - } - virtual bool shouldDisplay() const override; - -private slots: - void on_upgradeButton_clicked(); - -private: - void runModalTask(Task *task); - -private: - Ui::LegacyUpgradePage *ui; - InstancePtr m_inst; -}; diff --git a/launcher/ui/pages/instance/LegacyUpgradePage.ui b/launcher/ui/pages/instance/LegacyUpgradePage.ui deleted file mode 100644 index b22c03e5..00000000 --- a/launcher/ui/pages/instance/LegacyUpgradePage.ui +++ /dev/null @@ -1,54 +0,0 @@ - - - LegacyUpgradePage - - - - 0 - 0 - 546 - 405 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Noto Sans'; font-size:11pt; font-weight:400; font-style:normal;"> -<h1 style=" margin-top:18px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:xx-large; font-weight:600;">Upgrade is required</span></h1> -<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">PolyMC now supports old Minecraft versions and all the required features in the new (OneSix) instance format. As a consequence, the old (Legacy) format has been entirely disabled and old instances need to be upgraded.</p> -<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The upgrade will create a new instance with the same contents as the current one, in the new format. The original instance will remain untouched, in case anything goes wrong in the process.</p> -<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Please report any issues on our <a href="https://github.com/PolyMC/PolyMC/issues"><span style=" text-decoration: underline; color:#3584e4;">github issues page</span></a>.</p></body></html> - - - true - - - - - - - Upgrade the instance - - - - - - - - From 624ab25cd490833fa43743137918f7014c9fe376 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 21 Feb 2022 22:47:19 +0100 Subject: [PATCH 023/605] refactor: set default InstanceType to OneSix --- launcher/BaseInstance.cpp | 1 + launcher/InstanceCopyTask.cpp | 1 - launcher/InstanceCreationTask.cpp | 2 -- launcher/InstanceImportTask.cpp | 3 --- launcher/InstanceList.cpp | 2 +- launcher/modplatform/atlauncher/ATLPackInstallTask.cpp | 2 -- launcher/modplatform/legacy_ftb/PackInstallTask.cpp | 2 -- launcher/modplatform/modpacksch/FTBPackInstallTask.cpp | 2 -- launcher/modplatform/technic/TechnicPackProcessor.cpp | 2 -- 9 files changed, 2 insertions(+), 15 deletions(-) diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 374d4a29..1bff9e1d 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -39,6 +39,7 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("lastLaunchTime", 0); m_settings->registerSetting("totalTimePlayed", 0); m_settings->registerSetting("lastTimePlayed", 0); + m_settings->registerSetting("InstanceType", "OneSix"); // Custom Commands auto commandSetting = m_settings->registerSetting({"OverrideCommands","OverrideLaunchCmd"}, false); diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index 35adeaf9..c2bfe839 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -42,7 +42,6 @@ void InstanceCopyTask::copyFinished() } // FIXME: shouldn't this be able to report errors? auto instanceSettings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); - instanceSettings->registerSetting("InstanceType", "Legacy"); InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath)); inst->setName(m_instName); diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp index eafc5126..4c37bd7f 100644 --- a/launcher/InstanceCreationTask.cpp +++ b/launcher/InstanceCreationTask.cpp @@ -17,8 +17,6 @@ void InstanceCreationTask::executeTask() { auto instanceSettings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); instanceSettings->suspendSave(); - instanceSettings->registerSetting("InstanceType", "Legacy"); - instanceSettings->set("InstanceType", "OneSix"); MinecraftInstance inst(m_globalSettings, instanceSettings, m_stagingPath); auto components = inst.getPackProfile(); components->buildingFromScratch(); diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index ec378538..6dd615c7 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -261,8 +261,6 @@ void InstanceImportTask::processFlame() QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(configPath); - instanceSettings->registerSetting("InstanceType", "Legacy"); - instanceSettings->set("InstanceType", "OneSix"); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); auto mcVersion = pack.minecraft.version; // Hack to correct some 'special sauce'... @@ -422,7 +420,6 @@ void InstanceImportTask::processMultiMC() { QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(configPath); - instanceSettings->registerSetting("InstanceType", "Legacy"); NullInstance instance(m_globalSettings, instanceSettings, m_stagingPath); diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 5637983b..04b86f6b 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -544,7 +544,7 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) auto instanceSettings = std::make_shared(FS::PathCombine(instanceRoot, "instance.cfg")); InstancePtr inst; - instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->registerSetting("InstanceType", "Legacy"); // intentionally Legacy. We don't support it. QString inst_type = instanceSettings->get("InstanceType").toString(); diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 8de5fc9f..9dcb3504 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -720,8 +720,6 @@ void PackInstallTask::install() auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(instanceConfigPath); instanceSettings->suspendSave(); - instanceSettings->registerSetting("InstanceType", "Legacy"); - instanceSettings->set("InstanceType", "OneSix"); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); auto components = instance.getPackProfile(); diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 1d300192..f655a066 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -122,8 +122,6 @@ void PackInstallTask::install() QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(instanceConfigPath); instanceSettings->suspendSave(); - instanceSettings->registerSetting("InstanceType", "Legacy"); - instanceSettings->set("InstanceType", "OneSix"); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); auto components = instance.getPackProfile(); diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 03570226..33df6fa4 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -181,8 +181,6 @@ void PackInstallTask::install() auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(instanceConfigPath); instanceSettings->suspendSave(); - instanceSettings->registerSetting("InstanceType", "Legacy"); - instanceSettings->set("InstanceType", "OneSix"); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); auto components = instance.getPackProfile(); diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index c45061ac..156a295a 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -31,8 +31,6 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const QString minecraftPath = FS::PathCombine(stagingPath, ".minecraft"); QString configPath = FS::PathCombine(stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(configPath); - instanceSettings->registerSetting("InstanceType", "Legacy"); - instanceSettings->set("InstanceType", "OneSix"); MinecraftInstance instance(globalSettings, instanceSettings, stagingPath); instance.setName(instName); From 9c6727e27f55dd888c8cfc5147fdf0f6f8378f46 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 21 Feb 2022 21:34:06 -0300 Subject: [PATCH 024/605] feat: change task container in ModDownloadDialog to a QHash Previously, we used a unique_ptr to a ModDownloadTask to keep track of the selected mod to download when we accepted the dialog. In order to allow multiple mods to be selected at once for download, this has been changed to a QHash where the key is the mods name (since it doesn't seem right to allow for multiple versions of the same mod to be downloaded at once), and the value is a pointer to the corresponding ModDownloadTask. --- launcher/ModDownloadTask.h | 1 + launcher/ui/dialogs/ModDownloadDialog.cpp | 33 +++++++++++++++++--- launcher/ui/dialogs/ModDownloadDialog.h | 8 +++-- launcher/ui/pages/instance/ModFolderPage.cpp | 3 +- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/launcher/ModDownloadTask.h b/launcher/ModDownloadTask.h index 7e4f1b7d..ddada5a2 100644 --- a/launcher/ModDownloadTask.h +++ b/launcher/ModDownloadTask.h @@ -10,6 +10,7 @@ class ModDownloadTask : public Task { Q_OBJECT public: explicit ModDownloadTask(const QUrl sourceUrl, const QString filename, const std::shared_ptr mods); + const QString& getFilename() const { return filename; } public slots: bool abort() override; diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 6b807b8c..c2bf2d6f 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -52,6 +52,7 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr &mods HelpButton->setDefault(false); HelpButton->setAutoDefault(false); connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); + QMetaObject::connectSlotsByName(this); setWindowModality(Qt::WindowModal); setWindowTitle("Download mods"); @@ -83,16 +84,38 @@ QList ModDownloadDialog::getPages() }; } -void ModDownloadDialog::setSuggestedMod(const QString& name, ModDownloadTask* task) +void ModDownloadDialog::addSelectedMod(const QString& name, ModDownloadTask* task) { - modTask.reset(task); - m_buttons->button(QDialogButtonBox::Ok)->setEnabled(task); + if(modTask.contains(name)) + delete modTask.find(name).value(); + + if(task) + modTask.insert(name, task); + else + modTask.remove(name); + + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty()); +} + +void ModDownloadDialog::removeSelectedMod(const QString &name) +{ + if(modTask.contains(name)) + delete modTask.find(name).value(); + modTask.remove(name); +} + +bool ModDownloadDialog::isModSelected(const QString &name, const QString& filename) const +{ + // FIXME: Is there a way to check for versions without checking the filename + // as a heuristic, other than adding such info to ModDownloadTask itself? + auto iter = modTask.find(name); + return iter != modTask.end() && (iter.value()->getFilename() == filename); } ModDownloadDialog::~ModDownloadDialog() { } -ModDownloadTask *ModDownloadDialog::getTask() { - return modTask.release(); +const QList ModDownloadDialog::getTasks() { + return modTask.values(); } diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index ece8e328..02870c6c 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -29,9 +29,11 @@ public: QString dialogTitle() override; QList getPages() override; - void setSuggestedMod(const QString & name = QString(), ModDownloadTask * task = nullptr); + void addSelectedMod(const QString & name = QString(), ModDownloadTask * task = nullptr); + void removeSelectedMod(const QString & name = QString()); + bool isModSelected(const QString & name, const QString & filename) const; - ModDownloadTask * getTask(); + const QList getTasks(); const std::shared_ptr &mods; public slots: @@ -49,6 +51,6 @@ private: ModrinthPage *modrinthPage = nullptr; FlameModPage *flameModPage = nullptr; - std::unique_ptr modTask; + QHash modTask; BaseInstance *m_instance; }; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 494d32f0..b342accf 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -365,8 +365,7 @@ void ModFolderPage::on_actionInstall_mods_triggered() } ModDownloadDialog mdownload(m_mods, this, m_inst); if(mdownload.exec()) { - ModDownloadTask *task = mdownload.getTask(); - if (task) { + for(auto task : mdownload.getTasks()){ connect(task, &Task::failed, [this, task](QString reason) { task->deleteLater(); CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); From 512395e3f1ada7e16fa547f7fbe050e20a6d3209 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 21 Feb 2022 21:34:53 -0300 Subject: [PATCH 025/605] feat(ui): allow downloading multiple mods in Modrinth at once --- .../modplatform/modrinth/ModrinthPage.cpp | 54 ++++-- .../pages/modplatform/modrinth/ModrinthPage.h | 3 +- .../modplatform/modrinth/ModrinthPage.ui | 179 +++++++++--------- 3 files changed, 130 insertions(+), 106 deletions(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index c5a54c29..5b209fa3 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -34,6 +34,7 @@ ModrinthPage::ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance) connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); + connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthPage::onModSelected); } ModrinthPage::~ModrinthPage() @@ -61,7 +62,7 @@ bool ModrinthPage::shouldDisplay() const void ModrinthPage::openedImpl() { - suggestCurrent(); + updateSelectionButton(); triggerSearch(); } @@ -76,10 +77,6 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) if(!first.isValid()) { - if(isOpened) - { - dialog->setSuggestedMod(); - } return; } @@ -97,6 +94,10 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) if (!current.versionsLoaded) { qDebug() << "Loading Modrinth mod versions"; + + ui->modSelectionButton->setText(tr("Loading versions...")); + ui->modSelectionButton->setEnabled(false); + auto netJob = new NetJob(QString("Modrinth::ModVersions(%1)").arg(current.name), APPLICATION->network()); std::shared_ptr response = std::make_shared(); QString addonId = current.addonId; @@ -136,8 +137,9 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) ui->versionSelectionBox->addItem(tr("No Valid Version found !"), QVariant(-1)); } - suggestCurrent(); + updateSelectionButton(); }); + netJob->start(); } else @@ -148,33 +150,47 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) if(ui->versionSelectionBox->count() == 0){ ui->versionSelectionBox->addItem(tr("No Valid Version found !"), QVariant(-1)); } - suggestCurrent(); + + updateSelectionButton(); } } -void ModrinthPage::suggestCurrent() +void ModrinthPage::updateSelectionButton() { - if(!isOpened) - { + if(!isOpened || selectedVersion < 0){ + ui->modSelectionButton->setEnabled(false); return; } - if (selectedVersion == -1) - { - dialog->setSuggestedMod(); - return; + ui->modSelectionButton->setEnabled(true); + auto& version = current.versions[selectedVersion]; + if(!dialog->isModSelected(current.name, version.fileName)){ + ui->modSelectionButton->setText(tr("Select mod for download")); + } + else{ + ui->modSelectionButton->setText(tr("Deselect mod for download")); } - auto version = current.versions[selectedVersion]; - dialog->setSuggestedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName , dialog->mods)); } void ModrinthPage::onVersionSelectionChanged(QString data) { - if(data.isNull() || data.isEmpty()) - { + if (data.isNull() || data.isEmpty()){ selectedVersion = -1; return; } selectedVersion = ui->versionSelectionBox->currentData().toInt(); - suggestCurrent(); + updateSelectionButton(); +} + +void ModrinthPage::onModSelected() +{ + auto& version = current.versions[selectedVersion]; + if (dialog->isModSelected(current.name, version.fileName)){ + dialog->removeSelectedMod(current.name); + } + else{ + dialog->addSelectedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName , dialog->mods)); + } + + updateSelectionButton(); } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 3c517069..52b538e3 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -50,12 +50,13 @@ public: BaseInstance *m_instance; private: - void suggestCurrent(); + void updateSelectionButton(); private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(QString data); + void onModSelected(); private: Ui::ModrinthPage *ui = nullptr; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 6d183de5..d0a8b8f7 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -1,90 +1,97 @@ - ModrinthPage - - - - 0 - 0 - 837 - 685 - - - - - - - - - - 48 - 48 - - - - Qt::ScrollBarAlwaysOff - - - true - - - - - - - true - - - true - - - - - - - - - - - - - - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - Search - - - - - - - Search and filter ... - - - + ModrinthPage + + + + 0 + 0 + 837 + 685 + + + + + + + + + true + + + true + + + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + - - - searchEdit - searchButton - packView - packDescription - sortByBox - versionSelectionBox - - - + + + + + Search + + + + + + + Search and filter ... + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Select mod for download + + + + + + + + + searchEdit + searchButton + packView + packDescription + sortByBox + versionSelectionBox + + + From f5cf4eb45f6610851367bdcab7b87766bed14288 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 21 Feb 2022 21:53:21 -0300 Subject: [PATCH 026/605] feat(ui): allow downloading multiple mods from CurseForge at once --- .../pages/modplatform/flame/FlameModPage.cpp | 51 +++-- .../ui/pages/modplatform/flame/FlameModPage.h | 3 +- .../pages/modplatform/flame/FlameModPage.ui | 179 +++++++++--------- 3 files changed, 128 insertions(+), 105 deletions(-) diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index a816c681..728c3641 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -35,6 +35,7 @@ FlameModPage::FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance) connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged); + connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected); } FlameModPage::~FlameModPage() @@ -62,7 +63,7 @@ bool FlameModPage::shouldDisplay() const void FlameModPage::openedImpl() { - suggestCurrent(); + updateSelectionButton(); triggerSearch(); } @@ -77,10 +78,6 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) if(!first.isValid()) { - if(isOpened) - { - dialog->setSuggestedMod(); - } return; } @@ -112,6 +109,10 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) if (!current.versionsLoaded) { qDebug() << "Loading flame mod versions"; + + ui->modSelectionButton->setText(tr("Loading versions...")); + ui->modSelectionButton->setEnabled(false); + auto netJob = new NetJob(QString("Flame::ModVersions(%1)").arg(current.name), APPLICATION->network()); std::shared_ptr response = std::make_shared(); int addonId = current.addonId; @@ -151,7 +152,7 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) ui->versionSelectionBox->addItem(tr("No Valid Version found!"), QVariant(-1)); } - suggestCurrent(); + updateSelectionButton(); }); netJob->start(); } @@ -163,25 +164,26 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) if(ui->versionSelectionBox->count() == 0){ ui->versionSelectionBox->addItem(tr("No Valid Version found!"), QVariant(-1)); } - suggestCurrent(); + + updateSelectionButton(); } } -void FlameModPage::suggestCurrent() +void FlameModPage::updateSelectionButton() { - if(!isOpened) - { + if(!isOpened || selectedVersion < 0){ + ui->modSelectionButton->setEnabled(false); return; } - if (selectedVersion == -1) - { - dialog->setSuggestedMod(); - return; + ui->modSelectionButton->setEnabled(true); + auto& version = current.versions[selectedVersion]; + if(!dialog->isModSelected(current.name, version.fileName)){ + ui->modSelectionButton->setText(tr("Select mod for download")); + } + else{ + ui->modSelectionButton->setText(tr("Deselect mod for download")); } - - auto version = current.versions[selectedVersion]; - dialog->setSuggestedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName , dialog->mods)); } void FlameModPage::onVersionSelectionChanged(QString data) @@ -192,5 +194,18 @@ void FlameModPage::onVersionSelectionChanged(QString data) return; } selectedVersion = ui->versionSelectionBox->currentData().toInt(); - suggestCurrent(); + updateSelectionButton(); +} + +void FlameModPage::onModSelected() +{ + auto& version = current.versions[selectedVersion]; + if (dialog->isModSelected(current.name, version.fileName)){ + dialog->removeSelectedMod(current.name); + } + else{ + dialog->addSelectedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName , dialog->mods)); + } + + updateSelectionButton(); } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 8fa3248a..b5b19a4f 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -50,12 +50,13 @@ public: BaseInstance *m_instance; private: - void suggestCurrent(); + void updateSelectionButton(); private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(QString data); + void onModSelected(); private: Ui::FlameModPage *ui = nullptr; diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.ui b/launcher/ui/pages/modplatform/flame/FlameModPage.ui index 7da0bb4a..36df7e8a 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.ui +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.ui @@ -1,90 +1,97 @@ - FlameModPage - - - - 0 - 0 - 837 - 685 - - - - - - - - - - 48 - 48 - - - - Qt::ScrollBarAlwaysOff - - - true - - - - - - - true - - - true - - - - - - - - - - - - - - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - Search - - - - - - - Search and filter ... - - - + FlameModPage + + + + 0 + 0 + 837 + 685 + + + + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Select mod for download + + + - - - searchEdit - searchButton - packView - packDescription - sortByBox - versionSelectionBox - - - + + + + + Search and filter ... + + + + + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + + + true + + + true + + + + + + + + + Search + + + + + + + searchEdit + searchButton + packView + packDescription + sortByBox + versionSelectionBox + + + From 1004211a66370f2f39b81fdfc05e0e3645e91b90 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 21 Feb 2022 22:51:58 -0300 Subject: [PATCH 027/605] fix(ui): change text in selection button when there's no valid version --- launcher/ui/pages/modplatform/flame/FlameModPage.cpp | 1 + launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 728c3641..41ad9e28 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -152,6 +152,7 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) ui->versionSelectionBox->addItem(tr("No Valid Version found!"), QVariant(-1)); } + ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); updateSelectionButton(); }); netJob->start(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 5b209fa3..51372431 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -137,6 +137,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) ui->versionSelectionBox->addItem(tr("No Valid Version found !"), QVariant(-1)); } + ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); updateSelectionButton(); }); From 0102e9194075a53c2816966afb9d12b84ed4c276 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 21 Feb 2022 23:00:50 -0300 Subject: [PATCH 028/605] feat: add confirm dialog for installing mods When selecting multiple mods at once, it can become hard to keep track of which ones you selected. To address this, a dialog is now displayed when you finish selecting the mods to download, showing you which ones you selected and their filenames. From there, you can either accept it and download the mods, or you can cancel it and go back to the mod selection dialog. --- launcher/ui/dialogs/ModDownloadDialog.cpp | 28 ++++++++++++++++++++++- launcher/ui/dialogs/ModDownloadDialog.h | 1 + 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index c2bf2d6f..6240ecdc 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -5,6 +5,7 @@ #include #include "ProgressDialog.h" +#include "CustomMessageBox.h" #include #include @@ -41,7 +42,7 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr &mods auto OkButton = m_buttons->button(QDialogButtonBox::Ok); OkButton->setDefault(true); OkButton->setAutoDefault(true); - connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::accept); + connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::confirm); auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); CancelButton->setDefault(false); @@ -68,6 +69,31 @@ void ModDownloadDialog::reject() QDialog::reject(); } +void ModDownloadDialog::confirm() +{ + auto info = QString("You're about to download the following mods:\n\n"); + for(auto task : modTask.keys()){ + info.append(task); + info.append("\n --> File name: "); + info.append(modTask.find(task).value()->getFilename()); + info.append('\n'); + } + + auto confirm_dialog = CustomMessageBox::selectable( + this, + tr("Confirm mods to download"), + info, + QMessageBox::NoIcon, + {QMessageBox::Cancel, QMessageBox::Ok}, + QMessageBox::Ok + ); + + auto AcceptButton = confirm_dialog->button(QMessageBox::Ok); + connect(AcceptButton, &QPushButton::clicked, this, &ModDownloadDialog::accept); + + confirm_dialog->open(); +} + void ModDownloadDialog::accept() { QDialog::accept(); diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 02870c6c..309d89d0 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -37,6 +37,7 @@ public: const std::shared_ptr &mods; public slots: + void confirm(); void accept() override; void reject() override; From f8b0d6453ae81488ddfd4c83b329e2c88787c49e Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 21 Feb 2022 23:25:33 -0300 Subject: [PATCH 029/605] fix: sort mod list in confirmation dialog --- launcher/ui/dialogs/ModDownloadDialog.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 6240ecdc..439f22c4 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -71,8 +71,11 @@ void ModDownloadDialog::reject() void ModDownloadDialog::confirm() { + auto keys = modTask.keys(); + keys.sort(Qt::CaseInsensitive); + auto info = QString("You're about to download the following mods:\n\n"); - for(auto task : modTask.keys()){ + for(auto task : keys){ info.append(task); info.append("\n --> File name: "); info.append(modTask.find(task).value()->getFilename()); From f9d4751ec04963749c16c5d707ce7e5411b41ca3 Mon Sep 17 00:00:00 2001 From: txtsd Date: Tue, 22 Feb 2022 20:11:17 +0530 Subject: [PATCH 030/605] Use System Qt for generic Linux build --- .github/workflows/build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dbfcb82f..793b081e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -88,6 +88,7 @@ jobs: key: ${{ runner.os }}-${{ matrix.qt_version }}-${{ matrix.qt_arch }}-qt_cache - name: Install Qt + if: runner.os != 'Linux' || matrix.app_image == true uses: jurplel/install-qt-action@v2 with: version: ${{ matrix.qt_version }} @@ -96,6 +97,11 @@ jobs: cached: ${{ steps.cache-qt.outputs.cache-hit }} dir: "${{ github.workspace }}/Qt/" + - name: Install System Qt on Linux + if: runner.os == 'Linux' && matrix.app_image != true + run: | + sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 + - name: Install Ninja uses: urkle/action-get-ninja@v1 From 1e3b896fdaa8b557b648397d10166a52a284c4c7 Mon Sep 17 00:00:00 2001 From: txtsd Date: Sat, 12 Feb 2022 09:16:22 +0530 Subject: [PATCH 031/605] Replace layouts with LAUNCH_PORTABLE --- .github/workflows/build.yml | 2 +- BUILD.md | 14 +++--- CMakeLists.txt | 81 ++++++++++++--------------------- launcher/Application.cpp | 15 +++--- packages/nix/polymc/default.nix | 2 +- 5 files changed, 45 insertions(+), 69 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 793b081e..e4cc2614 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,7 +126,7 @@ jobs: - name: Configure CMake on Linux if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_LAYOUT=lin-system -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLAUNCHER_PORTABLE=OFF -G Ninja - name: Build run: | diff --git a/BUILD.md b/BUILD.md index 3b6e6446..42aed07a 100644 --- a/BUILD.md +++ b/BUILD.md @@ -56,7 +56,7 @@ This is the preferred method for installation, and is suitable for packages. cmake -S . -B build \   -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX="/usr" \ # Use "/usr" when building Linux packages. If building on FreeBSD or not for package, use "/usr/local" - -DLauncher_LAYOUT=lin-system + -DLAUNCHER_PORTABLE=OFF cd build make -j$(nproc) install # Optionally specify DESTDIR for packages (i.e. DESTDIR=${pkgdir}) ``` @@ -250,10 +250,11 @@ zlib1.dll 1. If you installed Qt with the web installer, there should be a shortcut called `Qt 5.4 for Desktop (MinGW 4.9 32-bit)` in the Start menu on Windows 7 and 10. Best way to find it is to search for it. Do note you cannot just use cmd.exe, you have to use the shortcut, otherwise the proper MinGW software will not be on the PATH. 2. Once that is open, change into your user directory, and clone PolyMC by doing `git clone --recursive https://github.com/PolyMC/PolyMC.git`, and change directory to the folder you cloned to. 3. Make a build directory, and change directory to the directory and do `cmake -G "MinGW Makefiles" -DCMAKE_INSTALL_PREFIX=C:\Path\that\makes\sense\for\you`. By default, it will install to C:\Program Files (x86), which you might not want, if you want a local installation. If you want to install it to that directory, make sure to run the command window as administrator. -4. Do `mingw32-make -jX`, where X is the number of cores your CPU has plus one. -5. Now to wait for it to compile. This could take some time. Hopefully it compiles properly. -6. Run the command `mingw32-make install`, and it should install PolyMC, to whatever the `-DCMAKE_INSTALL_PREFIX` was. -7. In most cases, whenever compiling, the OpenSSL dll's aren't put into the directory to where PolyMC installs, meaning you cannot log in. The best way to fix this is just to do `copy C:\OpenSSL-Win32\*.dll C:\Where\you\installed\PolyMC\to`. This should copy the required OpenSSL dll's to log in. +4. If you want PolyMC to store its data in `%APPDATA%`, append `-DLAUNCHER_PORTABLE=OFF` to the previous command. +5. Do `mingw32-make -jX`, where X is the number of cores your CPU has plus one. +6. Now to wait for it to compile. This could take some time. Hopefully it compiles properly. +7. Run the command `mingw32-make install`, and it should install PolyMC, to whatever the `-DCMAKE_INSTALL_PREFIX` was. +8. In most cases, whenever compiling, the OpenSSL dll's aren't put into the directory to where PolyMC installs, meaning you cannot log in. The best way to fix this is just to do `copy C:\OpenSSL-Win32\*.dll C:\Where\you\installed\PolyMC\to`. This should copy the required OpenSSL dll's to log in. # macOS @@ -288,7 +289,6 @@ cmake \ -DCMAKE_INSTALL_PREFIX:PATH="$(dirname $PWD)/dist/" \ -DCMAKE_PREFIX_PATH="/path/to/Qt/" \ -DQt5_DIR="/path/to/Qt/" \ - -DLauncher_LAYOUT=mac-bundle \ -DCMAKE_OSX_DEPLOYMENT_TARGET=10.7 \ .. make install @@ -335,7 +335,7 @@ This is the preferred method for installation, and is suitable for packages. cmake -S . -B build \   -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX="/usr/local" \ # /usr/local is default in OpenBSD and FreeBSD - -DLauncher_LAYOUT=lin-system -DCMAKE_PREFIX_PATH=/usr/local/lib/qt5/cmake # use linux layout and point to qt5 libs + -DLAUNCHER_PORTABLE=OFF -DCMAKE_PREFIX_PATH=/usr/local/lib/qt5/cmake # use linux layout and point to qt5 libs cd build make -j$(nproc) install # Optionally specify DESTDIR for packages (i.e. DESTDIR=${pkgdir}) ``` diff --git a/CMakeLists.txt b/CMakeLists.txt index 003aa65d..f98b8760 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,25 +146,11 @@ add_subdirectory(program_info) ####################################### Install layout ####################################### -# How to install the build results -set(Launcher_LAYOUT "auto" CACHE STRING "The layout for the launcher installation (auto, win-bundle, lin-nodeps, lin-system, mac-bundle)") -set_property(CACHE Launcher_LAYOUT PROPERTY STRINGS auto win-bundle lin-nodeps lin-system mac-bundle) +# Install the build results according to platform +set(LAUNCHER_PORTABLE 1 CACHE BOOL "The type of installation (Portable or System)") -if(Launcher_LAYOUT STREQUAL "auto") - if(UNIX AND APPLE) - set(Launcher_LAYOUT_REAL "mac-bundle") - elseif(UNIX) - set(Launcher_LAYOUT_REAL "lin-nodeps") - elseif(WIN32) - set(Launcher_LAYOUT_REAL "win-bundle") - else() - message(FATAL_ERROR "Cannot choose a sensible install layout for your platform.") - endif() -else() - set(Launcher_LAYOUT_REAL ${Launcher_LAYOUT}) -endif() -if(Launcher_LAYOUT_REAL STREQUAL "mac-bundle") +if(UNIX AND APPLE) set(BINARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") set(LIBRARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") set(PLUGIN_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") @@ -195,13 +181,32 @@ if(Launcher_LAYOUT_REAL STREQUAL "mac-bundle") # Add the icon install(FILES ${Launcher_Branding_ICNS} DESTINATION ${RESOURCES_DEST_DIR} RENAME ${Launcher_Name}.icns) -elseif(Launcher_LAYOUT_REAL STREQUAL "lin-nodeps") +elseif(UNIX) set(BINARY_DEST_DIR "bin") - set(LIBRARY_DEST_DIR "bin") - set(PLUGIN_DEST_DIR "plugins") - set(BUNDLE_DEST_DIR ".") - set(RESOURCES_DEST_DIR ".") - set(JARS_DEST_DIR "bin/jars") + if(LAUNCHER_PORTABLE) + set(LIBRARY_DEST_DIR "bin") + set(BUNDLE_DEST_DIR ".") + set(JARS_DEST_DIR "bin/jars") + + # launcher/Application.cpp will use this value + set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_PORTABLE=1") + + # Install basic runner script + configure_file(launcher/Launcher.in "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" @ONLY) + install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" DESTINATION ${BUNDLE_DEST_DIR} RENAME ${Launcher_Name}) + else() + set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}") + set(JARS_DEST_DIR "share/jars") + set(LAUNCHER_DESKTOP_DEST_DIR "share/applications" CACHE STRING "Path to the desktop file directory") + set(LAUNCHER_METAINFO_DEST_DIR "share/metainfo" CACHE STRING "Path to the metainfo directory") + set(LAUNCHER_ICON_DEST_DIR "share/icons/hicolor/scalable/apps" CACHE STRING "Path to the scalable icon directory") + + set(Launcher_APP_BINARY_DEFS "-DMULTIMC_JARS_LOCATION=${CMAKE_INSTALL_PREFIX}/${JARS_DEST_DIR}" "-DLAUNCHER_PORTABLE=0") + + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${LAUNCHER_DESKTOP_DEST_DIR}) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${LAUNCHER_METAINFO_DEST_DIR}) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION ${LAUNCHER_ICON_DEST_DIR}) + endif() # install as bundle with no dependencies included set(INSTALL_BUNDLE "nodeps") @@ -209,33 +214,7 @@ elseif(Launcher_LAYOUT_REAL STREQUAL "lin-nodeps") # Set RPATH SET(Launcher_BINARY_RPATH "$ORIGIN/") - # Install basic runner script - configure_file(launcher/Launcher.in "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" @ONLY) - install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" DESTINATION ${BUNDLE_DEST_DIR} RENAME ${Launcher_Name}) - -elseif(Launcher_LAYOUT_REAL STREQUAL "lin-system") - set(Launcher_BINARY_DEST_DIR "bin" CACHE STRING "Path to the binary directory") - set(Launcher_LIBRARY_DEST_DIR "lib${LIB_SUFFIX}" CACHE STRING "Path to the library directory") - set(Launcher_SHARE_DEST_DIR "share/polymc" CACHE STRING "Path to the shared data directory") - set(JARS_DEST_DIR "${Launcher_SHARE_DEST_DIR}/jars") - set(Launcher_DESKTOP_DEST_DIR "share/applications" CACHE STRING "Path to the desktop file directory") - set(Launcher_METAINFO_DEST_DIR "share/metainfo" CACHE STRING "Path to the metainfo directory") - set(Launcher_ICON_DEST_DIR "share/icons/hicolor/scalable/apps" CACHE STRING "Path to the scalable icon directory") - - set(BINARY_DEST_DIR ${Launcher_BINARY_DEST_DIR}) - set(LIBRARY_DEST_DIR ${Launcher_LIBRARY_DEST_DIR}) - - MESSAGE(STATUS "Compiling for linux system with ${Launcher_SHARE_DEST_DIR} and LAUNCHER_LINUX_DATADIR") - SET(Launcher_APP_BINARY_DEFS "-DMULTIMC_JARS_LOCATION=${CMAKE_INSTALL_PREFIX}/${JARS_DEST_DIR}" "-DLAUNCHER_LINUX_DATADIR") - - install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${Launcher_DESKTOP_DEST_DIR}) - install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${Launcher_METAINFO_DEST_DIR}) - install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION ${Launcher_ICON_DEST_DIR}) - - # install as bundle with no dependencies included - set(INSTALL_BUNDLE "nodeps") - -elseif(Launcher_LAYOUT_REAL STREQUAL "win-bundle") +elseif(WIN32) set(BINARY_DEST_DIR ".") set(LIBRARY_DEST_DIR ".") set(PLUGIN_DEST_DIR ".") @@ -252,7 +231,7 @@ elseif(Launcher_LAYOUT_REAL STREQUAL "win-bundle") # install as bundle set(INSTALL_BUNDLE "full") else() - message(FATAL_ERROR "No sensible install layout set.") + message(FATAL_ERROR "Platform not supported") endif() ################################ Included Libs ################################ diff --git a/launcher/Application.cpp b/launcher/Application.cpp index e33df252..16fd612e 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -294,16 +294,11 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) } else { -#ifdef LAUNCHER_LINUX_DATADIR - QString xdgDataHome = QFile::decodeName(qgetenv("XDG_DATA_HOME")); - if (xdgDataHome.isEmpty()) - xdgDataHome = QDir::homePath() + QLatin1String("/.local/share"); - dataPath = xdgDataHome + "/polymc"; - adjustedBy += "XDG standard " + dataPath; -#elif defined(Q_OS_MAC) + // qDebug() << LAUNCHER_PORTABLE; +#if !LAUNCHER_PORTABLE || defined(Q_OS_MAC) QDir foo(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); dataPath = foo.absolutePath(); - adjustedBy += "Fallback to special Mac location " + dataPath; + adjustedBy += dataPath; #else dataPath = applicationDirPath(); adjustedBy += "Fallback to binary path " + dataPath; @@ -505,8 +500,10 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) #elif defined(Q_OS_WIN32) m_rootPath = binPath; #elif defined(Q_OS_MAC) - QDir foo(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); + QDir foo(FS::PathCombine(binPath, "../..")); m_rootPath = foo.absolutePath(); + // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues) + FS::updateTimestamp(m_rootPath); #endif #ifdef MULTIMC_JARS_LOCATION diff --git a/packages/nix/polymc/default.nix b/packages/nix/polymc/default.nix index a9610017..cc2ed4db 100644 --- a/packages/nix/polymc/default.nix +++ b/packages/nix/polymc/default.nix @@ -69,7 +69,7 @@ mkDerivation rec { cmakeFlags = [ "-GNinja" - "-DLauncher_LAYOUT=lin-system" + "-DLAUNCHER_PORTABLE=OFF" ]; postInstall = '' From 69d01204e0e1220e196d4a7cb6cfdb41292bc6a9 Mon Sep 17 00:00:00 2001 From: txtsd Date: Tue, 15 Feb 2022 00:42:14 +0530 Subject: [PATCH 032/605] Implement PR suggestions --- .github/workflows/build.yml | 2 +- BUILD.md | 8 ++++---- CMakeLists.txt | 8 ++++---- launcher/Application.cpp | 3 +-- packages/nix/polymc/default.nix | 2 +- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e4cc2614..af5a1074 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,7 +126,7 @@ jobs: - name: Configure CMake on Linux if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLAUNCHER_PORTABLE=OFF -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_PORTABLE=OFF -G Ninja - name: Build run: | diff --git a/BUILD.md b/BUILD.md index 42aed07a..fa6a8d04 100644 --- a/BUILD.md +++ b/BUILD.md @@ -56,7 +56,7 @@ This is the preferred method for installation, and is suitable for packages. cmake -S . -B build \   -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX="/usr" \ # Use "/usr" when building Linux packages. If building on FreeBSD or not for package, use "/usr/local" - -DLAUNCHER_PORTABLE=OFF + -DLauncher_PORTABLE=OFF cd build make -j$(nproc) install # Optionally specify DESTDIR for packages (i.e. DESTDIR=${pkgdir}) ``` @@ -250,8 +250,8 @@ zlib1.dll 1. If you installed Qt with the web installer, there should be a shortcut called `Qt 5.4 for Desktop (MinGW 4.9 32-bit)` in the Start menu on Windows 7 and 10. Best way to find it is to search for it. Do note you cannot just use cmd.exe, you have to use the shortcut, otherwise the proper MinGW software will not be on the PATH. 2. Once that is open, change into your user directory, and clone PolyMC by doing `git clone --recursive https://github.com/PolyMC/PolyMC.git`, and change directory to the folder you cloned to. 3. Make a build directory, and change directory to the directory and do `cmake -G "MinGW Makefiles" -DCMAKE_INSTALL_PREFIX=C:\Path\that\makes\sense\for\you`. By default, it will install to C:\Program Files (x86), which you might not want, if you want a local installation. If you want to install it to that directory, make sure to run the command window as administrator. -4. If you want PolyMC to store its data in `%APPDATA%`, append `-DLAUNCHER_PORTABLE=OFF` to the previous command. -5. Do `mingw32-make -jX`, where X is the number of cores your CPU has plus one. +4. If you want PolyMC to store its data in `%APPDATA%`, append `-DLauncher_PORTABLE=OFF` to the previous command. +5. Do `mingw32-make -j$(nproc)`, where X is the number of cores your CPU has plus one. 6. Now to wait for it to compile. This could take some time. Hopefully it compiles properly. 7. Run the command `mingw32-make install`, and it should install PolyMC, to whatever the `-DCMAKE_INSTALL_PREFIX` was. 8. In most cases, whenever compiling, the OpenSSL dll's aren't put into the directory to where PolyMC installs, meaning you cannot log in. The best way to fix this is just to do `copy C:\OpenSSL-Win32\*.dll C:\Where\you\installed\PolyMC\to`. This should copy the required OpenSSL dll's to log in. @@ -335,7 +335,7 @@ This is the preferred method for installation, and is suitable for packages. cmake -S . -B build \   -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX="/usr/local" \ # /usr/local is default in OpenBSD and FreeBSD - -DLAUNCHER_PORTABLE=OFF -DCMAKE_PREFIX_PATH=/usr/local/lib/qt5/cmake # use linux layout and point to qt5 libs + -DLauncher_PORTABLE=OFF -DCMAKE_PREFIX_PATH=/usr/local/lib/qt5/cmake # use linux layout and point to qt5 libs cd build make -j$(nproc) install # Optionally specify DESTDIR for packages (i.e. DESTDIR=${pkgdir}) ``` diff --git a/CMakeLists.txt b/CMakeLists.txt index f98b8760..02900f83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,7 +147,7 @@ add_subdirectory(program_info) ####################################### Install layout ####################################### # Install the build results according to platform -set(LAUNCHER_PORTABLE 1 CACHE BOOL "The type of installation (Portable or System)") +set(Launcher_PORTABLE 1 CACHE BOOL "The type of installation (Portable or System)") if(UNIX AND APPLE) @@ -183,13 +183,13 @@ if(UNIX AND APPLE) elseif(UNIX) set(BINARY_DEST_DIR "bin") - if(LAUNCHER_PORTABLE) + if(Launcher_PORTABLE) set(LIBRARY_DEST_DIR "bin") set(BUNDLE_DEST_DIR ".") set(JARS_DEST_DIR "bin/jars") # launcher/Application.cpp will use this value - set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_PORTABLE=1") + set(Launcher_APP_BINARY_DEFS "-DLauncher_PORTABLE=1") # Install basic runner script configure_file(launcher/Launcher.in "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" @ONLY) @@ -201,7 +201,7 @@ elseif(UNIX) set(LAUNCHER_METAINFO_DEST_DIR "share/metainfo" CACHE STRING "Path to the metainfo directory") set(LAUNCHER_ICON_DEST_DIR "share/icons/hicolor/scalable/apps" CACHE STRING "Path to the scalable icon directory") - set(Launcher_APP_BINARY_DEFS "-DMULTIMC_JARS_LOCATION=${CMAKE_INSTALL_PREFIX}/${JARS_DEST_DIR}" "-DLAUNCHER_PORTABLE=0") + set(Launcher_APP_BINARY_DEFS "-DMULTIMC_JARS_LOCATION=${CMAKE_INSTALL_PREFIX}/${JARS_DEST_DIR}" "-DLauncher_PORTABLE=0") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${LAUNCHER_DESKTOP_DEST_DIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${LAUNCHER_METAINFO_DEST_DIR}) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 16fd612e..fa5d267e 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -294,8 +294,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) } else { - // qDebug() << LAUNCHER_PORTABLE; -#if !LAUNCHER_PORTABLE || defined(Q_OS_MAC) +#if !Launcher_PORTABLE || defined(Q_OS_MAC) QDir foo(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); dataPath = foo.absolutePath(); adjustedBy += dataPath; diff --git a/packages/nix/polymc/default.nix b/packages/nix/polymc/default.nix index cc2ed4db..d65f39e1 100644 --- a/packages/nix/polymc/default.nix +++ b/packages/nix/polymc/default.nix @@ -69,7 +69,7 @@ mkDerivation rec { cmakeFlags = [ "-GNinja" - "-DLAUNCHER_PORTABLE=OFF" + "-DLauncher_PORTABLE=OFF" ]; postInstall = '' From ca8b62291fe47554effaf625b5c52807802a58a9 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 14 Feb 2022 20:20:11 +0100 Subject: [PATCH 033/605] fix: use legacy data path if it exists --- launcher/Application.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index fa5d267e..bf644ea6 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -298,6 +298,16 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) QDir foo(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); dataPath = foo.absolutePath(); adjustedBy += dataPath; + +#ifdef Q_OS_LINUX + // TODO: this should be removed in a future version + // TODO: provide a migration path similar to macOS migration + QDir bar(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation), "polymc")); + if (bar.exists()) { + dataPath = bar.absolutePath(); + adjustedBy += "Legacy data path " + dataPath; + } +#endif #else dataPath = applicationDirPath(); adjustedBy += "Fallback to binary path " + dataPath; From 04840d0638f933416a0d14b2ef822c6f8da5e373 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 23 Feb 2022 14:44:55 -0300 Subject: [PATCH 034/605] fix(ui): add translation func to text in the confirm dialog --- launcher/ui/dialogs/ModDownloadDialog.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 439f22c4..a56c2e64 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -74,10 +74,12 @@ void ModDownloadDialog::confirm() auto keys = modTask.keys(); keys.sort(Qt::CaseInsensitive); - auto info = QString("You're about to download the following mods:\n\n"); + auto info = QString(tr("You're about to download the following mods:")); + info.append("\n\n"); for(auto task : keys){ info.append(task); - info.append("\n --> File name: "); + info.append("\n --> "); + info.append(tr("File name: ")); info.append(modTask.find(task).value()->getFilename()); info.append('\n'); } From 40a9828fbab0e9a8b7f070714bdd87d3f279e18c Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 23 Feb 2022 19:17:33 -0300 Subject: [PATCH 035/605] fix: improve readability and set ok button as disabled by default --- launcher/ui/dialogs/ModDownloadDialog.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index a56c2e64..23ca8731 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -40,6 +40,7 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr &mods // Bonk Qt over its stupid head and make sure it understands which button is the default one... // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button auto OkButton = m_buttons->button(QDialogButtonBox::Ok); + OkButton->setEnabled(false); OkButton->setDefault(true); OkButton->setAutoDefault(true); connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::confirm); @@ -89,7 +90,7 @@ void ModDownloadDialog::confirm() tr("Confirm mods to download"), info, QMessageBox::NoIcon, - {QMessageBox::Cancel, QMessageBox::Ok}, + QMessageBox::Cancel | QMessageBox::Ok, QMessageBox::Ok ); @@ -117,13 +118,8 @@ QList ModDownloadDialog::getPages() void ModDownloadDialog::addSelectedMod(const QString& name, ModDownloadTask* task) { - if(modTask.contains(name)) - delete modTask.find(name).value(); - - if(task) - modTask.insert(name, task); - else - modTask.remove(name); + removeSelectedMod(name); + modTask.insert(name, task); m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty()); } @@ -133,6 +129,8 @@ void ModDownloadDialog::removeSelectedMod(const QString &name) if(modTask.contains(name)) delete modTask.find(name).value(); modTask.remove(name); + + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty()); } bool ModDownloadDialog::isModSelected(const QString &name, const QString& filename) const From 4835ec3f6d8967e097f301ef806e94de82b866f6 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 25 Feb 2022 15:19:15 +0100 Subject: [PATCH 036/605] fix(i18n): update translations URL --- buildconfig/BuildConfig.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 111381ab..1c5ff357 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -98,7 +98,7 @@ public: QString AUTH_BASE = "https://authserver.mojang.com/"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; QString FMLLIBS_BASE_URL = "https://files.multimc.org/fmllibs/"; - QString TRANSLATIONS_BASE_URL = "https://meta.polymc.org/translations/"; + QString TRANSLATIONS_BASE_URL = "https://i18n.polymc.org/"; QString MODPACKSCH_API_BASE_URL = "https://api.modpacks.ch/"; From e8929599a52a7df3a67b76fd7bf586a219030247 Mon Sep 17 00:00:00 2001 From: Babbaj Date: Fri, 25 Feb 2022 16:47:47 -0500 Subject: [PATCH 037/605] nix: Use POLYMC_JAVA_PATHS --- packages/nix/polymc/default.nix | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/nix/polymc/default.nix b/packages/nix/polymc/default.nix index a9610017..9a986be4 100644 --- a/packages/nix/polymc/default.nix +++ b/packages/nix/polymc/default.nix @@ -47,12 +47,7 @@ mkDerivation rec { dontWrapQtApps = true; - postPatch = '' - # hardcode jdk paths - substituteInPlace launcher/java/JavaUtils.cpp \ - --replace 'scanJavaDir("/usr/lib/jvm")' 'javas.append("${jdk}/lib/openjdk/bin/java")' \ - --replace 'scanJavaDir("/usr/lib32/jvm")' 'javas.append("${jdk8}/lib/openjdk/bin/java")' - '' + lib.optionalString (msaClientID != "") '' + postPatch = lib.optionalString (msaClientID != "") '' # add client ID substituteInPlace CMakeLists.txt \ --replace '17b47edd-c884-4997-926d-9e7f9a6b4647' '${msaClientID}' @@ -77,6 +72,7 @@ mkDerivation rec { wrapProgram $out/bin/polymc \ "''${qtWrapperArgs[@]}" \ --set GAME_LIBRARY_PATH ${gameLibraryPath} \ + --prefix POLYMC_JAVA_PATHS : ${jdk}/lib/openjdk/bin/java:${jdk8}/lib/openjdk/bin/java \ --prefix PATH : ${lib.makeBinPath [ xorg.xrandr ]} ''; From c9bf7f9896af6e864048b9c024d777c47e770f6e Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 25 Feb 2022 23:23:08 +0100 Subject: [PATCH 038/605] fix: load instances no matter their instance type --- launcher/InstanceList.cpp | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 04b86f6b..6e37e3d8 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -543,19 +543,8 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) auto instanceRoot = FS::PathCombine(m_instDir, id); auto instanceSettings = std::make_shared(FS::PathCombine(instanceRoot, "instance.cfg")); InstancePtr inst; - - instanceSettings->registerSetting("InstanceType", "Legacy"); // intentionally Legacy. We don't support it. - - QString inst_type = instanceSettings->get("InstanceType").toString(); - - if (inst_type == "OneSix" || inst_type == "Nostalgia") - { - inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); - } - else - { - inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot)); - } + // TODO: Handle incompatible instances + inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); qDebug() << "Loaded instance " << inst->name() << " from " << inst->instanceRoot(); return inst; } From 4f975bfb04ec4ae70ebbec2ddb2644f994104259 Mon Sep 17 00:00:00 2001 From: glowiak <52356948+glowiak@users.noreply.github.com> Date: Sat, 26 Feb 2022 13:04:35 +0100 Subject: [PATCH 039/605] Notify about needed AppBSD version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 50bd88b9..d9050b7c 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ MacOS has experimental development builds available [here](https://github.com/Po There are community-maintained binary packages available: -- [AppBSD Image](http://glowiak.github.io/file/polymc-latest-fbsd64-appbsd) - a portable application, requires [AppBSD](https://codeberg.org/glowiak/appbsd/) to be installed. +- [AppBSD Image](http://glowiak.github.io/file/polymc-latest-fbsd64-appbsd) - a portable application, requires [AppBSD](https://codeberg.org/glowiak/appbsd/) (0.4.9 or newer) to be installed. - [Gzipped binaries](http://glowiak.github.io/file/polymc-latest-fbsd64-raw) - traditional way to distribute, unpack and run. From 2745325ae007315694064b158daa65a63abb8ad4 Mon Sep 17 00:00:00 2001 From: timoreo Date: Sun, 27 Feb 2022 11:55:24 +0100 Subject: [PATCH 040/605] Fixed wrong version info --- launcher/ui/pages/modplatform/flame/FlameModPage.cpp | 5 ++++- launcher/ui/pages/modplatform/flame/FlamePage.cpp | 5 ++++- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 6d33a6ac..114ac907 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -118,7 +118,10 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) { .arg(addonId), response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response] { + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] { + if(addonId != current.addonId){ + return; //wrong request + } QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 1138a298..c46b98b5 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -114,8 +114,11 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) int addonId = current.addonId; netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response.get())); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response] + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] { + if(addonId != current.addonId){ + return; //wrong request + } QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index fc6aff96..35cd743a 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -103,7 +103,10 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId), response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response] { + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] { + if(addonId != current.addonId){ + return; + } QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { From ccc493cb2bd1873152c1cc94d81604d4e80f1225 Mon Sep 17 00:00:00 2001 From: timoreo Date: Sun, 27 Feb 2022 13:14:12 +0100 Subject: [PATCH 041/605] Cleanly free NetJob in flame modpack --- launcher/ui/pages/modplatform/flame/FlamePage.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index c46b98b5..7e6ac2fd 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -109,10 +109,10 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) if (current.versionsLoaded == false) { qDebug() << "Loading flame modpack versions"; - NetJob *netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network()); - std::shared_ptr response = std::make_shared(); + auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network()); + auto response = new QByteArray(); int addonId = current.addonId; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response.get())); + netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] { @@ -143,6 +143,11 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) suggestCurrent(); }); + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] + { + netJob->deleteLater(); + delete response; + }); netJob->start(); } else From 5d1ca33b84afee9f09093c27f2eff525325d08cb Mon Sep 17 00:00:00 2001 From: txtsd Date: Sun, 27 Feb 2022 08:35:47 -0800 Subject: [PATCH 042/605] Apply suggestions from code review Co-authored-by: LennyMcLennington --- CMakeLists.txt | 4 ++-- launcher/Application.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 02900f83..c370b6ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -189,7 +189,7 @@ elseif(UNIX) set(JARS_DEST_DIR "bin/jars") # launcher/Application.cpp will use this value - set(Launcher_APP_BINARY_DEFS "-DLauncher_PORTABLE=1") + set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_PORTABLE") # Install basic runner script configure_file(launcher/Launcher.in "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" @ONLY) @@ -201,7 +201,7 @@ elseif(UNIX) set(LAUNCHER_METAINFO_DEST_DIR "share/metainfo" CACHE STRING "Path to the metainfo directory") set(LAUNCHER_ICON_DEST_DIR "share/icons/hicolor/scalable/apps" CACHE STRING "Path to the scalable icon directory") - set(Launcher_APP_BINARY_DEFS "-DMULTIMC_JARS_LOCATION=${CMAKE_INSTALL_PREFIX}/${JARS_DEST_DIR}" "-DLauncher_PORTABLE=0") + set(Launcher_APP_BINARY_DEFS "-DMULTIMC_JARS_LOCATION=${CMAKE_INSTALL_PREFIX}/${JARS_DEST_DIR}") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${LAUNCHER_DESKTOP_DEST_DIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${LAUNCHER_METAINFO_DEST_DIR}) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index bf644ea6..0ce80d4b 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -294,7 +294,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) } else { -#if !Launcher_PORTABLE || defined(Q_OS_MAC) +#if !defined(LAUNCHER_PORTABLE) || defined(Q_OS_MAC) QDir foo(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); dataPath = foo.absolutePath(); adjustedBy += dataPath; From 075d900d45f25475c7fe600d6237f17d5c257d30 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 27 Feb 2022 15:05:38 -0300 Subject: [PATCH 043/605] fix: Always tell Flame API which modloader we are using Fixes #206 partially. Although we don't list mods that have no compatibility with the mod loader we are using, mods that have support for both loaders still show up, and the versions for both the loaders are still shown. Also simplifies a little the logic in FlameModIndex::loadIndexedPackVersions --- launcher/modplatform/flame/FlameModIndex.cpp | 32 ++++++++++--------- .../pages/modplatform/flame/FlameModModel.cpp | 4 +-- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index a8b2495a..082ffa57 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -50,42 +50,44 @@ void FlameMod::loadIndexedPackVersions(FlameMod::IndexedPack & pack, QJsonArray for(auto versionIter: arr) { auto obj = versionIter.toObject(); - FlameMod::IndexedVersion file; - file.addonId = pack.addonId; - file.fileId = Json::requireInteger(obj, "id"); - file.date = Json::requireString(obj, "fileDate"); + auto versionArray = Json::requireArray(obj, "gameVersion"); - if (versionArray.empty()) { + if (versionArray.isEmpty()) { continue; } + + FlameMod::IndexedVersion file; for(auto mcVer : versionArray){ file.mcVersion.append(mcVer.toString()); } + file.addonId = pack.addonId; + file.fileId = Json::requireInteger(obj, "id"); + file.date = Json::requireString(obj, "fileDate"); file.version = Json::requireString(obj, "displayName"); file.downloadUrl = Json::requireString(obj, "downloadUrl"); file.fileName = Json::requireString(obj, "fileName"); auto modules = Json::requireArray(obj, "modules"); - bool valid = false; + bool is_valid_fabric_version = false; for(auto m : modules){ auto fname = Json::requireString(m.toObject(),"foldername"); + // FIXME: This does not work properly when a mod supports more than one mod loader, since + // they bundle the meta files for all of them in the same arquive, even when that version + // doesn't support the given mod loader. if(hasFabric){ if(fname == "fabric.mod.json"){ - valid = true; - break; - } - }else{ - //this cannot check for the recent mcmod.toml formats - if(fname == "mcmod.info"){ - valid = true; + is_valid_fabric_version = true; break; } } + else if(fname == "mcmod.info"){ //this cannot check for the recent mcmod.toml formats + break; + } } - if(!valid && hasFabric){ + + if(hasFabric && !is_valid_fabric_version) continue; - } unsortedVersions.append(file); } diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index 2cf83261..e8afba5a 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -175,13 +175,13 @@ void ListModel::performPaginatedSearch() "pageSize=25&" "searchFilter=%2&" "sort=%3&" - "%4" + "modLoaderType=%4&" "gameVersion=%5" ) .arg(nextSearchOffset) .arg(currentSearchTerm) .arg(sorts[currentSort]) - .arg(hasFabric ? "modLoaderType=4&" : "") + .arg(hasFabric ? 4 : 1) // Enum: https://docs.curseforge.com/?http#tocS_ModLoaderType .arg(mcVersion); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); From 148775b3e97f67547af88e0ced643c042bbe28fc Mon Sep 17 00:00:00 2001 From: Lenny McLennington Date: Sun, 27 Feb 2022 19:40:52 +0000 Subject: [PATCH 044/605] Update FMLLIBS_BASE_URL Updated FMLLIBS_BASE_URL to https://files.polymc.org/fmllibs/ Fixes #208 --- buildconfig/BuildConfig.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 1c5ff357..863f88df 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -97,7 +97,7 @@ public: QString LIBRARY_BASE = "https://libraries.minecraft.net/"; QString AUTH_BASE = "https://authserver.mojang.com/"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; - QString FMLLIBS_BASE_URL = "https://files.multimc.org/fmllibs/"; + QString FMLLIBS_BASE_URL = "https://files.polymc.org/fmllibs/"; QString TRANSLATIONS_BASE_URL = "https://i18n.polymc.org/"; QString MODPACKSCH_API_BASE_URL = "https://api.modpacks.ch/"; From 4e8f075ff3c62292e2aff2aa5ddf59aaa18cdf05 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 27 Feb 2022 22:02:43 -0300 Subject: [PATCH 045/605] fix: Do not loop when its not a fabric mod on Flame version validation Since there's no validation for forge mods since the start, we were just looping with no porpuse in this situation. --- launcher/modplatform/flame/FlameModIndex.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 082ffa57..4adaf5f1 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -81,9 +81,8 @@ void FlameMod::loadIndexedPackVersions(FlameMod::IndexedPack & pack, QJsonArray break; } } - else if(fname == "mcmod.info"){ //this cannot check for the recent mcmod.toml formats - break; - } + else break; + // NOTE: Since we're not validating forge versions, we can just skip this loop. } if(hasFabric && !is_valid_fabric_version) From 10231aa404513b06a9663b973bdb7d23423d71a5 Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Tue, 1 Mar 2022 10:33:59 +1000 Subject: [PATCH 046/605] Modify readme to use website --- README.md | 109 ++---------------------------------------------------- 1 file changed, 4 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index d9050b7c..d8a9c7aa 100644 --- a/README.md +++ b/README.md @@ -11,113 +11,9 @@ This is a **fork** of the MultiMC Launcher and not endorsed by MultiMC. The Poly # Installation -- All packages (archived by version) can be found [here](https://packages.polymc.org/) ([latest](https://packages.polymc.org/latest)) +- all downloads and instructions for polymc can be found [here](https://polymc.org/download/) - Last build status: https://jenkins.polymc.org/job/PolyMC/lastBuild/ -## 🐧 Linux - -### Cross-distro packages - -Download on Flathub - -Download as AppImage - -- [AppImage SHA256](https://packages.polymc.org/latest/appimage/PolyMC-latest-x86_64.AppImage.sha256) - -### Arch Linux - -There are several AUR packages available: - -[![polymc](https://img.shields.io/badge/aur-polymc-blue)](https://aur.archlinux.org/packages/polymc/) -[![polymc-bin](https://img.shields.io/badge/aur-polymc--bin-blue)](https://aur.archlinux.org/packages/polymc-bin/) - -```sh -# source package: -yay -S polymc -# binary package: -yay -S polymc-bin -``` - -### Debian and Ubuntu - -We use [makedeb](https://docs.makedeb.org/) for our Debian packages. -Several MPR packages are available: - -[![polymc](https://img.shields.io/badge/mpr-polymc-orange)](https://mpr.makedeb.org/packages/polymc) -[![polymc-bin](https://img.shields.io/badge/mpr-polymc--bin-orange)](https://mpr.makedeb.org/packages/polymc-bin) - -```sh -# source package: -sudo tap install polymc -# binary package: -sudo tap install polymc-bin -``` - -### Nix - -A [Nix derivation](packages/nix/NIX.md) is available. - -### Gentoo - -A Gentoo ebuild is available in the [swirl](https://git.swurl.xyz/swirl/ebuilds) overlay, named `games-action/polymc`. - -```sh -# as root: -emerge --oneshot eselect-repository -eselect repository enable swirl -emaint sync -r swirl -emerge polymc -# to use latest git version: -sudo tee -a /etc/portage/package.accept_keywords <<< "=games-action/polymc-9999 **" -``` - -### Fedora - -An RPM package is available on [COPR](https://copr.fedorainfracloud.org/coprs/polymc/polymc/). - -```sh -sudo dnf copr enable polymc/polymc -sudo dnf install polymc -``` - -### Slackware - -[A SlackBuild](https://codeberg.org/glowiak/SlackBuilds/src/branch/master/repository/polymc.md) is available. You will need [qt5](http://slackbuilds.org/repository/14.2/libraries/qt5/) (on 15.0 installed by default), [a JDK](https://codeberg.org/glowiak/SlackBuilds/src/branch/master/repository/adoptium-jdk8.md), and if you're on 14.2, you need to compile newer CMake version manually. To build, type in extracted directory with all dependiences met: - - sudo ./polymc.SlackBuild - sudo installpkg /tmp/polymc-version-arch-1_SBo.tgz - -You can also download a community-maintained [prebuilt x86_64 package](http://glowiak.github.io/file/polymc-latest-slackware) and install it with /sbin/installpkg: - - sudo /sbin/installpkg ~/Downloads/polymc-version-x86_64-1_SBo.tgz - -## Windows - -[Windows (32-bit)](https://packages.polymc.org/latest/win32/win32.zip) ([SHA256](https://packages.polymc.org/latest/win32/win32.zip.sha256)) - this is a portable package, you can extract it anywhere and run it. This package needs testing. - -## MacOS - -MacOS has experimental development builds available [here](https://github.com/PolyMC/PolyMC/actions) - -## FreeBSD - -There are community-maintained binary packages available: - -- [AppBSD Image](http://glowiak.github.io/file/polymc-latest-fbsd64-appbsd) - a portable application, requires [AppBSD](https://codeberg.org/glowiak/appbsd/) (0.4.9 or newer) to be installed. - -- [Gzipped binaries](http://glowiak.github.io/file/polymc-latest-fbsd64-raw) - traditional way to distribute, unpack and run. - -In both cases you need X11, Qt5 and Java installed. Both files are 64bit only. -You can build from source - see [BUILD.md](./BUILD.md) - -## OpenBSD - -There are community-maintained binary packages available: - -- [gzipped 32-bit binaries](http://glowiak.github.io/file/polymc-latest-obsd32-raw), download, unpack and run. - -You need X11, Qt5 and Java installed. -You can build from source - see [BUILD.md](./BUILD.md) ## Development Builds @@ -170,6 +66,9 @@ In general, in order of importance: The translation effort for PolyMC is hosted on [Weblate](https://hosted.weblate.org/projects/polymc/polymc/) and information about translating PolyMC is available at https://github.com/PolyMC/Translations +## Download infomation +To modify download infomation or change packaging infomation send a pull request or issue to the website [Here](https://github.com/PolyMC/polymc.github.io/blob/master/src/download.md) + ## Forking/Redistributing/Custom builds policy Do whatever you want, we don't care. Just follow the license. If you have any questions about this feel free to ask in an issue. From ebececf8c682c77e51fc1ea29c8ea353ac10ba3b Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Tue, 1 Mar 2022 23:19:30 +1000 Subject: [PATCH 047/605] Update and link latest build to actions --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d8a9c7aa..5d83a8f9 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ This is a **fork** of the MultiMC Launcher and not endorsed by MultiMC. The Poly # Installation -- all downloads and instructions for polymc can be found [here](https://polymc.org/download/) -- Last build status: https://jenkins.polymc.org/job/PolyMC/lastBuild/ +- All downloads and instructions for PolyMC can be found [here](https://polymc.org/download/) +- Last build status: https://github.com/PolyMC/PolyMC/actions ## Development Builds From 3a03f908312afe86222215232cf2e32079e2649d Mon Sep 17 00:00:00 2001 From: txtsd Date: Wed, 2 Mar 2022 01:17:08 +0530 Subject: [PATCH 048/605] Ignore certain paths for CI --- .github/workflows/trigger_builds.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 1dfc728e..1561b9d6 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -4,7 +4,17 @@ on: push: branches-ignore: - 'stable' + paths-ignore: + - '**.md' + - '**/LICENSE' + - 'flake.lock' + - '**.nix' pull_request: + paths-ignore: + - '**.md' + - '**/LICENSE' + - 'flake.lock' + - '**.nix' workflow_dispatch: jobs: From 881b2f2b385f19d9b64a149fca3c3741a803a172 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 2 Mar 2022 18:35:59 -0300 Subject: [PATCH 049/605] refactor: Use a single indexed pack for mods Since there's little difference between them, let's remove duplication and merge them. --- launcher/modplatform/ModIndex.h | 42 +++++++++++ launcher/modplatform/flame/FlameModIndex.cpp | 62 ++++++++--------- launcher/modplatform/flame/FlameModIndex.h | 48 +++---------- .../modrinth/ModrinthPackIndex.cpp | 69 +++++++++---------- .../modplatform/modrinth/ModrinthPackIndex.h | 48 +++---------- .../pages/modplatform/flame/FlameModModel.cpp | 6 +- .../pages/modplatform/flame/FlameModModel.h | 2 +- .../pages/modplatform/flame/FlameModPage.cpp | 6 +- .../ui/pages/modplatform/flame/FlameModPage.h | 2 +- .../modplatform/modrinth/ModrinthModel.cpp | 6 +- .../modplatform/modrinth/ModrinthModel.h | 2 +- .../modplatform/modrinth/ModrinthPage.cpp | 8 +-- .../pages/modplatform/modrinth/ModrinthPage.h | 2 +- 13 files changed, 137 insertions(+), 166 deletions(-) create mode 100644 launcher/modplatform/ModIndex.h diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h new file mode 100644 index 00000000..7e1cf254 --- /dev/null +++ b/launcher/modplatform/ModIndex.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace ModPlatform { + +struct ModpackAuthor { + QString name; + QString url; +}; + +struct IndexedVersion { + QVariant addonId; + QVariant fileId; + QString version; + QVector mcVersion; + QString downloadUrl; + QString date; + QString fileName; + QVector loaders = {}; +}; + +struct IndexedPack { + QVariant addonId; + QString name; + QString description; + QList authors; + QString logoName; + QString logoUrl; + QString websiteUrl; + + bool versionsLoaded = false; + QVector versions; +}; + +} // namespace ModPlatform + +Q_DECLARE_METATYPE(ModPlatform::IndexedPack) diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 4adaf5f1..61cb534c 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -1,13 +1,11 @@ -#include #include "FlameModIndex.h" + #include "Json.h" -#include "net/NetJob.h" -#include "BaseInstance.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "net/NetJob.h" - -void FlameMod::loadIndexedPack(FlameMod::IndexedPack & pack, QJsonObject & obj) +void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); pack.name = Json::requireString(obj, "name"); @@ -16,10 +14,10 @@ void FlameMod::loadIndexedPack(FlameMod::IndexedPack & pack, QJsonObject & obj) bool thumbnailFound = false; auto attachments = Json::requireArray(obj, "attachments"); - for(auto attachmentRaw: attachments) { + for (auto attachmentRaw : attachments) { auto attachmentObj = Json::requireObject(attachmentRaw); bool isDefault = attachmentObj.value("isDefault").toBool(false); - if(isDefault) { + if (isDefault) { thumbnailFound = true; pack.logoName = Json::requireString(attachmentObj, "title"); pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl"); @@ -27,37 +25,35 @@ void FlameMod::loadIndexedPack(FlameMod::IndexedPack & pack, QJsonObject & obj) } } - if(!thumbnailFound) { - throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); - } - + if (!thumbnailFound) { throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); } auto authors = Json::requireArray(obj, "authors"); - for(auto authorIter: authors) { + for (auto authorIter : authors) { auto author = Json::requireObject(authorIter); - FlameMod::ModpackAuthor packAuthor; + ModPlatform::ModpackAuthor packAuthor; packAuthor.name = Json::requireString(author, "name"); packAuthor.url = Json::requireString(author, "url"); pack.authors.append(packAuthor); } } -void FlameMod::loadIndexedPackVersions(FlameMod::IndexedPack & pack, QJsonArray & arr, const shared_qobject_ptr& network, BaseInstance * inst) +void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, + QJsonArray& arr, + const shared_qobject_ptr& network, + BaseInstance* inst) { - QVector unsortedVersions; - bool hasFabric = !((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); - QString mcVersion = ((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.minecraft"); + QVector unsortedVersions; + bool hasFabric = !((MinecraftInstance*)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); + QString mcVersion = ((MinecraftInstance*)inst)->getPackProfile()->getComponentVersion("net.minecraft"); - for(auto versionIter: arr) { + for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto versionArray = Json::requireArray(obj, "gameVersion"); - if (versionArray.isEmpty()) { - continue; - } + if (versionArray.isEmpty()) { continue; } - FlameMod::IndexedVersion file; - for(auto mcVer : versionArray){ + ModPlatform::IndexedVersion file; + for (auto mcVer : versionArray) { file.mcVersion.append(mcVer.toString()); } @@ -70,29 +66,27 @@ void FlameMod::loadIndexedPackVersions(FlameMod::IndexedPack & pack, QJsonArray auto modules = Json::requireArray(obj, "modules"); bool is_valid_fabric_version = false; - for(auto m : modules){ - auto fname = Json::requireString(m.toObject(),"foldername"); + for (auto m : modules) { + auto fname = Json::requireString(m.toObject(), "foldername"); // FIXME: This does not work properly when a mod supports more than one mod loader, since // they bundle the meta files for all of them in the same arquive, even when that version // doesn't support the given mod loader. - if(hasFabric){ - if(fname == "fabric.mod.json"){ + if (hasFabric) { + if (fname == "fabric.mod.json") { is_valid_fabric_version = true; break; } - } - else break; + } else + break; // NOTE: Since we're not validating forge versions, we can just skip this loop. } - if(hasFabric && !is_valid_fabric_version) - continue; + if (hasFabric && !is_valid_fabric_version) continue; unsortedVersions.append(file); } - auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool - { - //dates are in RFC 3339 format + 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); diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index 0293bb23..34f71498 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -3,48 +3,18 @@ // #pragma once -#include -#include -#include -#include + +#include "modplatform/ModIndex.h" + #include -#include -#include "net/NetJob.h" #include "BaseInstance.h" namespace FlameMod { - struct ModpackAuthor { - QString name; - QString url; - }; - struct IndexedVersion { - int addonId; - int fileId; - QString version; - QVector mcVersion; - QString downloadUrl; - QString date; - QString fileName; - }; +void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, + QJsonArray& arr, + const shared_qobject_ptr& network, + BaseInstance* inst); - struct IndexedPack - { - int addonId; - QString name; - QString description; - QList authors; - QString logoName; - QString logoUrl; - QString websiteUrl; - - bool versionsLoaded = false; - QVector versions; - }; - - void loadIndexedPack(IndexedPack & m, QJsonObject & obj); - void loadIndexedPackVersions(IndexedPack &pack, QJsonArray &arr, const shared_qobject_ptr &network, BaseInstance *inst); - -} - -Q_DECLARE_METATYPE(FlameMod::IndexedPack) +} // namespace FlameMod diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 9017eb67..a59186f7 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -1,14 +1,11 @@ -#include #include "ModrinthPackIndex.h" #include "Json.h" -#include "net/NetJob.h" -#include "BaseInstance.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "net/NetJob.h" - -void Modrinth::loadIndexedPack(Modrinth::IndexedPack & pack, QJsonObject & obj) +void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireString(obj, "project_id"); pack.name = Json::requireString(obj, "title"); @@ -16,59 +13,60 @@ void Modrinth::loadIndexedPack(Modrinth::IndexedPack & pack, QJsonObject & obj) pack.description = Json::ensureString(obj, "description", ""); pack.logoUrl = Json::requireString(obj, "icon_url"); - pack.logoName = pack.addonId; + pack.logoName = pack.addonId.toString(); - Modrinth::ModpackAuthor modAuthor; + ModPlatform::ModpackAuthor modAuthor; modAuthor.name = Json::requireString(obj, "author"); - modAuthor.url = "https://modrinth.com/user/"+modAuthor.name; - pack.author = modAuthor; + modAuthor.url = "https://modrinth.com/user/" + modAuthor.name; + pack.authors.append(modAuthor); } -void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray & arr, const shared_qobject_ptr& network, BaseInstance * inst) +void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, + QJsonArray& arr, + const shared_qobject_ptr& network, + BaseInstance* inst) { - QVector unsortedVersions; - bool hasFabric = !((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); - QString mcVersion = ((MinecraftInstance *)inst)->getPackProfile()->getComponentVersion("net.minecraft"); + QVector unsortedVersions; + bool hasFabric = !((MinecraftInstance*)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); + QString mcVersion = ((MinecraftInstance*)inst)->getPackProfile()->getComponentVersion("net.minecraft"); - for(auto versionIter: arr) { + for (auto versionIter : arr) { auto obj = versionIter.toObject(); - Modrinth::IndexedVersion file; - file.addonId = Json::requireString(obj,"project_id") ; + ModPlatform::IndexedVersion file; + file.addonId = Json::requireString(obj, "project_id"); file.fileId = Json::requireString(obj, "id"); file.date = Json::requireString(obj, "date_published"); auto versionArray = Json::requireArray(obj, "game_versions"); - if (versionArray.empty()) { - continue; - } - for(auto mcVer : versionArray){ + if (versionArray.empty()) { continue; } + for (auto mcVer : versionArray) { file.mcVersion.append(mcVer.toString()); } - auto loaders = Json::requireArray(obj,"loaders"); - for(auto loader : loaders){ + auto loaders = Json::requireArray(obj, "loaders"); + for (auto loader : loaders) { file.loaders.append(loader.toString()); } file.version = Json::requireString(obj, "name"); auto files = Json::requireArray(obj, "files"); int i = 0; - while (files.count() > 1 && i < files.count()){ - //try to resolve the correct file + while (files.count() > 1 && i < files.count()) { + // try to resolve the correct file auto parent = files[i].toObject(); auto fileName = Json::requireString(parent, "filename"); - //avoid grabbing "dev" files - if(fileName.contains("javadocs",Qt::CaseInsensitive) || fileName.contains("sources",Qt::CaseInsensitive)){ + // avoid grabbing "dev" files + if (fileName.contains("javadocs", Qt::CaseInsensitive) || fileName.contains("sources", Qt::CaseInsensitive)) { i++; continue; } - //grab the correct mod loader - if(fileName.contains("forge",Qt::CaseInsensitive) || fileName.contains("fabric",Qt::CaseInsensitive) ){ - if(hasFabric){ - if(fileName.contains("forge",Qt::CaseInsensitive)){ + // grab the correct mod loader + if (fileName.contains("forge", Qt::CaseInsensitive) || fileName.contains("fabric", Qt::CaseInsensitive)) { + if (hasFabric) { + if (fileName.contains("forge", Qt::CaseInsensitive)) { i++; continue; } - }else{ - if(fileName.contains("fabric",Qt::CaseInsensitive)){ + } else { + if (fileName.contains("fabric", Qt::CaseInsensitive)) { i++; continue; } @@ -77,16 +75,15 @@ void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray break; } auto parent = files[i].toObject(); - if(parent.contains("url")) { + if (parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); file.fileName = Json::requireString(parent, "filename"); unsortedVersions.append(file); } } - auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool - { - //dates are in RFC 3339 format + 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); diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index 3a4cd270..abfdabb6 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -1,48 +1,16 @@ #pragma once -#include -#include -#include -#include +#include "modplatform/ModIndex.h" + #include -#include -#include "net/NetJob.h" #include "BaseInstance.h" namespace Modrinth { -struct ModpackAuthor { - QString name; - QString url; -}; +void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, + QJsonArray& arr, + const shared_qobject_ptr& network, + BaseInstance* inst); -struct IndexedVersion { - QString addonId; - QString fileId; - QString version; - QVector mcVersion; - QString downloadUrl; - QString date; - QString fileName; - QVector loaders; -}; - -struct IndexedPack -{ - QString addonId; - QString name; - QString description; - ModpackAuthor author; - QString logoName; - QString logoUrl; - QString websiteUrl; - - bool versionsLoaded = false; - QVector versions; -}; - -void loadIndexedPack(IndexedPack & m, QJsonObject & obj); -void loadIndexedPackVersions(IndexedPack &pack, QJsonArray &arr, const shared_qobject_ptr &network, BaseInstance *inst); -} - -Q_DECLARE_METATYPE(Modrinth::IndexedPack) +} // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index e8afba5a..052f30d1 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -39,7 +39,7 @@ QVariant ListModel::data(const QModelIndex &index, int role) const return QString("INVALID INDEX %1").arg(pos); } - IndexedPack pack = modpacks.at(pos); + ModPlatform::IndexedPack pack = modpacks.at(pos); if(role == Qt::DisplayRole) { return pack.name; @@ -225,12 +225,12 @@ void ListModel::searchRequestFinished() return; } - QList newList; + QList newList; auto packs = doc.array(); for(auto packRaw : packs) { auto packObj = packRaw.toObject(); - FlameMod::IndexedPack pack; + ModPlatform::IndexedPack pack; try { FlameMod::loadIndexedPack(pack, packObj); diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h index 0c1cb95e..ec19ab2f 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h @@ -57,7 +57,7 @@ private: void requestLogo(QString file, QString url); private: - QList modpacks; + QList modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; LogoMap m_logoMap; diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 114ac907..48d38b82 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -77,7 +77,7 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) { return; } - current = listModel->data(first, Qt::UserRole).value(); + current = listModel->data(first, Qt::UserRole).value(); QString text = ""; QString name = current.name; @@ -86,7 +86,7 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) { else text = "" + name + ""; if (!current.authors.empty()) { - auto authorToStr = [](FlameMod::ModpackAuthor &author) { + auto authorToStr = [](ModPlatform::ModpackAuthor &author) { if (author.url.isEmpty()) { return author.name; } @@ -112,7 +112,7 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) { new NetJob(QString("Flame::ModVersions(%1)").arg(current.name), APPLICATION->network()); auto response = new QByteArray(); - int addonId = current.addonId; + int addonId = current.addonId.toInt(); netJob->addNetAction(Net::Download::makeByteArray( QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files") .arg(addonId), diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index b5b19a4f..24dfcbc7 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -62,7 +62,7 @@ private: Ui::FlameModPage *ui = nullptr; ModDownloadDialog* dialog = nullptr; FlameMod::ListModel* listModel = nullptr; - FlameMod::IndexedPack current; + ModPlatform::IndexedPack current; int selectedVersion = -1; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 5a18830a..088ca411 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -41,7 +41,7 @@ QVariant ListModel::data(const QModelIndex &index, int role) const return QString("INVALID INDEX %1").arg(pos); } - IndexedPack pack = modpacks.at(pos); + ModPlatform::IndexedPack pack = modpacks.at(pos); if(role == Qt::DisplayRole) { return pack.name; @@ -222,12 +222,12 @@ void Modrinth::ListModel::searchRequestFinished() return; } - QList newList; + QList newList; auto packs = doc.object().value("hits").toArray(); for(auto packRaw : packs) { auto packObj = packRaw.toObject(); - Modrinth::IndexedPack pack; + ModPlatform::IndexedPack pack; try { Modrinth::loadIndexedPack(pack, packObj); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 53f1f134..20661412 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -57,7 +57,7 @@ private: void requestLogo(QString file, QString url); private: - QList modpacks; + QList modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; LogoMap m_logoMap; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 35cd743a..4dfc8504 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -76,7 +76,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { return; } - current = listModel->data(first, Qt::UserRole).value(); + current = listModel->data(first, Qt::UserRole).value(); QString text = ""; QString name = current.name; @@ -84,8 +84,8 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { text = name; else text = "" + name + ""; - text += "
" + tr(" by ") + "" + - current.author.name + "

"; + text += "
" + tr(" by ") + "" + + current.authors[0].name + "

"; ui->packDescription->setHtml(text + current.description); if (!current.versionsLoaded) { @@ -98,7 +98,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { new NetJob(QString("Modrinth::ModVersions(%1)").arg(current.name), APPLICATION->network()); auto response = new QByteArray(); - QString addonId = current.addonId; + QString addonId = current.addonId.toString(); netJob->addNetAction(Net::Download::makeByteArray( QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId), response)); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 52b538e3..1d725d47 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -62,7 +62,7 @@ private: Ui::ModrinthPage *ui = nullptr; ModDownloadDialog* dialog = nullptr; Modrinth::ListModel* listModel = nullptr; - Modrinth::IndexedPack current; + ModPlatform::IndexedPack current; int selectedVersion = -1; }; From 0dd1c26cf3cd68cd83f5d9da6cf34d4aa3f30db2 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 2 Mar 2022 21:17:10 -0300 Subject: [PATCH 050/605] refactor: extract common code in mod pages and model This creates a hierarchy in which ModPage and ModModel are the parents of every mod provider, providing the basic functionality common to all of them. It also imposes a unique .ui file (they were already equal before, just duplicated basically) on all mod providers. --- launcher/CMakeLists.txt | 8 +- launcher/ui/pages/modplatform/ModModel.cpp | 166 ++++++++++++ launcher/ui/pages/modplatform/ModModel.h | 60 +++++ launcher/ui/pages/modplatform/ModPage.cpp | 152 +++++++++++ launcher/ui/pages/modplatform/ModPage.h | 57 ++++ .../{modrinth/ModrinthPage.ui => ModPage.ui} | 4 +- .../pages/modplatform/flame/FlameModModel.cpp | 223 ++-------------- .../pages/modplatform/flame/FlameModModel.h | 68 +---- .../pages/modplatform/flame/FlameModPage.cpp | 229 +++------------- .../ui/pages/modplatform/flame/FlameModPage.h | 72 +---- .../pages/modplatform/flame/FlameModPage.ui | 97 ------- .../modplatform/modrinth/ModrinthModel.cpp | 245 ++---------------- .../modplatform/modrinth/ModrinthModel.h | 68 +---- .../modplatform/modrinth/ModrinthPage.cpp | 215 +++------------ .../pages/modplatform/modrinth/ModrinthPage.h | 72 +---- 15 files changed, 623 insertions(+), 1113 deletions(-) create mode 100644 launcher/ui/pages/modplatform/ModModel.cpp create mode 100644 launcher/ui/pages/modplatform/ModModel.h create mode 100644 launcher/ui/pages/modplatform/ModPage.cpp create mode 100644 launcher/ui/pages/modplatform/ModPage.h rename launcher/ui/pages/modplatform/{modrinth/ModrinthPage.ui => ModPage.ui} (97%) delete mode 100644 launcher/ui/pages/modplatform/flame/FlameModPage.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 86c05651..0dcda925 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -721,6 +721,11 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/VanillaPage.cpp ui/pages/modplatform/VanillaPage.h + ui/pages/modplatform/ModPage.cpp + ui/pages/modplatform/ModPage.h + ui/pages/modplatform/ModModel.cpp + ui/pages/modplatform/ModModel.h + ui/pages/modplatform/atlauncher/AtlFilterModel.cpp ui/pages/modplatform/atlauncher/AtlFilterModel.h ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -879,13 +884,12 @@ qt5_wrap_ui(LAUNCHER_UI ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui ui/pages/modplatform/atlauncher/AtlPage.ui ui/pages/modplatform/VanillaPage.ui + ui/pages/modplatform/ModPage.ui ui/pages/modplatform/flame/FlamePage.ui - ui/pages/modplatform/flame/FlameModPage.ui ui/pages/modplatform/legacy_ftb/Page.ui ui/pages/modplatform/ImportPage.ui ui/pages/modplatform/ftb/FtbPage.ui ui/pages/modplatform/technic/TechnicPage.ui - ui/pages/modplatform/modrinth/ModrinthPage.ui ui/widgets/InstanceCardWidget.ui ui/widgets/CustomCommands.ui ui/widgets/MCModInfoFrame.ui diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp new file mode 100644 index 00000000..5bd7e33e --- /dev/null +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -0,0 +1,166 @@ +#include "ModModel.h" +#include "ModPage.h" + +#include "ui/dialogs/ModDownloadDialog.h" + +#include + +namespace ModPlatform { + +ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) {} + +ListModel::~ListModel() {} + +int ListModel::rowCount(const QModelIndex& parent) const +{ + return modpacks.size(); +} + +int ListModel::columnCount(const QModelIndex& parent) const +{ + return 1; +} + +QVariant ListModel::data(const QModelIndex& index, int role) const +{ + int pos = index.row(); + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } + + ModPlatform::IndexedPack pack = modpacks.at(pos); + if (role == Qt::DisplayRole) { + return pack.name; + } else if (role == Qt::ToolTipRole) { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; + } else if (role == Qt::DecorationRole) { + 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); + return icon; + } else if (role == Qt::UserRole) { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +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) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } + } +} + +void ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +Qt::ItemFlags ListModel::flags(const QModelIndex& index) const +{ + return QAbstractListModel::flags(index); +} + +bool ListModel::canFetchMore(const QModelIndex& parent) const +{ + return searchState == CanPossiblyFetchMore; +} + +void ListModel::fetchMore(const QModelIndex& parent) +{ + if (parent.isValid()) return; + if (nextSearchOffset == 0) { + qWarning() << "fetchMore with 0 offset is wrong..."; + return; + } + performPaginatedSearch(); +} + +void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) +{ + if (m_logoMap.contains(logo)) { + callback(APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } else { + requestLogo(logo, logoUrl); + } +} + +void ListModel::searchWithTerm(const QString& term, const int sort) +{ + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { return; } + currentSearchTerm = term; + currentSort = sort; + if (jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + nextSearchOffset = 0; + performPaginatedSearch(); +} + +void ListModel::searchRequestFailed(QString reason) +{ + if (jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { + // 409 Gone, notify user to update + QMessageBox::critical(nullptr, tr("Error"), + QString("%1 %2") + .arg(m_parent->displayName()) + .arg(tr("API version too old!\nPlease update PolyMC!"))); + // self-destruct + ((ModDownloadDialog*)((ModPage*)parent())->parentWidget())->reject(); + } + jobPtr.reset(); + + if (searchState == ResetRequested) { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + nextSearchOffset = 0; + performPaginatedSearch(); + } else { + searchState = Finished; + } +} + +void ListModel::requestLogo(QString logo, QString url) +{ + if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; } + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); + auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + job->deleteLater(); + emit logoLoaded(logo, QIcon(fullPath)); + if (waitingCallbacks.contains(logo)) { waitingCallbacks.value(logo)(fullPath); } + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + job->deleteLater(); + emit logoFailed(logo); + }); + + job->start(); + m_loadingLogos.append(logo); +} + +} // namespace ModPlatform diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h new file mode 100644 index 00000000..ae8ceb7c --- /dev/null +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -0,0 +1,60 @@ +#pragma once + +#include + +#include "modplatform/ModIndex.h" +#include "net/NetJob.h" + +class ModPage; + +namespace ModPlatform { + +typedef QMap LogoMap; +typedef std::function LogoCallback; + +class ListModel : public QAbstractListModel { + Q_OBJECT + + public: + ListModel(ModPage* parent); + virtual ~ListModel(); + + int rowCount(const QModelIndex& parent) const override; + int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + bool canFetchMore(const QModelIndex& parent) const override; + void fetchMore(const QModelIndex& parent) override; + + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); + void searchWithTerm(const QString& term, const int sort); + + protected slots: + virtual void performPaginatedSearch() = 0; + virtual void searchRequestFinished() = 0; + + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + + void searchRequestFailed(QString reason); + + protected: + void requestLogo(QString file, QString url); + + protected: + ModPage* m_parent; + + QList modpacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + LogoMap m_logoMap; + QMap waitingCallbacks; + + QString currentSearchTerm; + int currentSort = 0; + int nextSearchOffset = 0; + enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; + NetJob::Ptr jobPtr; + QByteArray response; +}; +} // namespace ModPlatform diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp new file mode 100644 index 00000000..8af534a6 --- /dev/null +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -0,0 +1,152 @@ +#include "ModPage.h" +#include "ui_ModPage.h" + +#include + +#include "ui/dialogs/ModDownloadDialog.h" + +ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance) + : QWidget(dialog), m_instance(instance), ui(new Ui::ModPage), dialog(dialog) +{ + ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); + ui->searchEdit->installEventFilter(this); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + +} + +ModPage::~ModPage() +{ + delete ui; +} + +void ModPage::openedImpl() +{ + updateSelectionButton(); + triggerSearch(); +} + +bool ModPage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + +void ModPage::updateSelectionButton() +{ + if (!isOpened || selectedVersion < 0) { + ui->modSelectionButton->setEnabled(false); + return; + } + + ui->modSelectionButton->setEnabled(true); + auto& version = current.versions[selectedVersion]; + if (!dialog->isModSelected(current.name, version.fileName)) { + ui->modSelectionButton->setText(tr("Select mod for download")); + } else { + ui->modSelectionButton->setText(tr("Deselect mod for download")); + } +} + +void ModPage::triggerSearch() +{ + listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); +} + +void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if (!first.isValid()) { return; } + + current = listModel->data(first, Qt::UserRole).value(); + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name; + else + text = "" + name + ""; + + if (!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) { + authorStrs.push_back(authorToStr(author)); + } + text += "
" + tr(" by ") + authorStrs.join(", "); + } + text += "

"; + + ui->packDescription->setHtml(text + current.description); + + if (!current.versionsLoaded) { + qDebug() << QString("Loading %1 mod versions").arg(debugName()); + + ui->modSelectionButton->setText(tr("Loading versions...")); + ui->modSelectionButton->setEnabled(false); + + auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(debugName()).arg(current.name), APPLICATION->network()); + auto response = new QByteArray(); + QString addonId = current.addonId.toString(); + //FIXME + if(debugName() == "Modrinth") + netJob->addNetAction( + Net::Download::makeByteArray(QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId), response)); + else + netJob->addNetAction( + Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response)); + + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId]{ + onModVersionSucceed(this, response, addonId); + }); + + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { + netJob->deleteLater(); + delete response; + }); + + netJob->start(); + } else { + for (int i = 0; i < current.versions.size(); i++) { + ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i)); + } + if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No Valid Version found !"), QVariant(-1)); } + + updateSelectionButton(); + } +} + +void ModPage::onVersionSelectionChanged(QString data) +{ + if (data.isNull() || data.isEmpty()) { + selectedVersion = -1; + return; + } + selectedVersion = ui->versionSelectionBox->currentData().toInt(); + updateSelectionButton(); +} + +void ModPage::onModSelected() +{ + auto& version = current.versions[selectedVersion]; + if (dialog->isModSelected(current.name, version.fileName)) { + dialog->removeSelectedMod(current.name); + } else { + dialog->addSelectedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName, dialog->mods)); + } + + updateSelectionButton(); +} diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h new file mode 100644 index 00000000..c0102549 --- /dev/null +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +#include "modplatform/ModIndex.h" +#include "tasks/Task.h" +#include "ui/pages/BasePage.h" +#include "ui/pages/modplatform/ModModel.h" + +class ModDownloadDialog; + +namespace Ui { +class ModPage; +} + +class ModPage : public QWidget, public BasePage { + Q_OBJECT + + public: + explicit ModPage(ModDownloadDialog* dialog, BaseInstance* instance); + virtual ~ModPage(); + + inline virtual QString displayName() const override = 0; + inline virtual QIcon icon() const override = 0; + inline virtual QString id() const override = 0; + inline virtual QString helpPage() const override = 0; + + inline virtual QString debugName() const = 0; + inline virtual QString metaEntryBase() const = 0; + + virtual bool shouldDisplay() const override = 0; + + void openedImpl() override; + bool eventFilter(QObject* watched, QEvent* event) override; + + BaseInstance* m_instance; + + protected: + virtual void onModVersionSucceed(ModPage*, QByteArray*, QString) = 0; + + void updateSelectionButton(); + + protected slots: + void triggerSearch(); + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + void onModSelected(); + + protected: + Ui::ModPage* ui = nullptr; + ModDownloadDialog* dialog = nullptr; + ModPlatform::ListModel* listModel = nullptr; + ModPlatform::IndexedPack current; + + int selectedVersion = -1; +}; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/ModPage.ui similarity index 97% rename from launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui rename to launcher/ui/pages/modplatform/ModPage.ui index d0a8b8f7..2b52df22 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/ModPage.ui @@ -1,7 +1,7 @@ - ModrinthPage - + ModPage + 0 diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index 052f30d1..5ab6672f 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -1,169 +1,25 @@ #include "FlameModModel.h" -#include "Application.h" +#include "FlameModPage.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "FlameModPage.h" + #include -#include -#include - -#include - - namespace FlameMod { -ListModel::ListModel(FlameModPage *parent) : QAbstractListModel(parent) -{ -} +ListModel::ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {} -ListModel::~ListModel() -{ -} +ListModel::~ListModel() {} -int ListModel::rowCount(const QModelIndex &parent) const -{ - return modpacks.size(); -} - -int ListModel::columnCount(const QModelIndex &parent) const -{ - return 1; -} - -QVariant ListModel::data(const QModelIndex &index, int role) const -{ - int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { - return QString("INVALID INDEX %1").arg(pos); - } - - ModPlatform::IndexedPack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { - return pack.name; - } - else if (role == Qt::ToolTipRole) - { - if(pack.description.length() > 100) - { - //some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); - edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); - return edit; - - } - return pack.description; - } - else if(role == Qt::DecorationRole) - { - 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); - return icon; - } - else if(role == Qt::UserRole) - { - QVariant v; - v.setValue(pack); - return v; - } - - return QVariant(); -} - -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) { - emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); - } - } -} - -void ListModel::logoFailed(QString logo) -{ - m_failedLogos.append(logo); - m_loadingLogos.removeAll(logo); -} - -void ListModel::requestLogo(QString logo, QString url) -{ - if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) - { - return; - } - - MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0))); - auto job = new NetJob(QString("Flame Icon Download %1").arg(logo), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); - - auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] - { - job->deleteLater(); - emit logoLoaded(logo, QIcon(fullPath)); - if(waitingCallbacks.contains(logo)) - { - waitingCallbacks.value(logo)(fullPath); - } - }); - - QObject::connect(job, &NetJob::failed, this, [this, logo, job] - { - job->deleteLater(); - emit logoFailed(logo); - }); - - job->start(); - m_loadingLogos.append(logo); -} - -void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) -{ - if(m_logoMap.contains(logo)) - { - callback(APPLICATION->metacache()->resolveEntry("FlameMods", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); - } - else - { - requestLogo(logo, logoUrl); - } -} - -Qt::ItemFlags ListModel::flags(const QModelIndex &index) const -{ - return QAbstractListModel::flags(index); -} - -bool ListModel::canFetchMore(const QModelIndex& parent) const -{ - return searchState == CanPossiblyFetchMore; -} - -void ListModel::fetchMore(const QModelIndex& parent) -{ - if (parent.isValid()) - return; - if(nextSearchOffset == 0) { - qWarning() << "fetchMore with 0 offset is wrong..."; - return; - } - performPaginatedSearch(); -} -const char* sorts[6]{"Featured","Popularity","LastUpdated","Name","Author","TotalDownloads"}; +const char* sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; void ListModel::performPaginatedSearch() { - - QString mcVersion = ((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); - bool hasFabric = !((MinecraftInstance *)((FlameModPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); + QString mcVersion = ((MinecraftInstance*)((FlameModPage*)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); + bool hasFabric = !((MinecraftInstance*)((FlameModPage*)parent())->m_instance) + ->getPackProfile() + ->getComponentVersion("net.fabricmc.fabric-loader") + .isEmpty(); auto netJob = new NetJob("Flame::Search", APPLICATION->network()); auto searchUrl = QString( "https://addons-ecs.forgesvc.net/api/v2/addon/search?" @@ -176,44 +32,22 @@ void ListModel::performPaginatedSearch() "searchFilter=%2&" "sort=%3&" "modLoaderType=%4&" - "gameVersion=%5" - ) - .arg(nextSearchOffset) - .arg(currentSearchTerm) - .arg(sorts[currentSort]) - .arg(hasFabric ? 4 : 1) // Enum: https://docs.curseforge.com/?http#tocS_ModLoaderType - .arg(mcVersion); + "gameVersion=%5") + .arg(nextSearchOffset) + .arg(currentSearchTerm) + .arg(sorts[currentSort]) + .arg(hasFabric ? 4 : 1) // Enum: https://docs.curseforge.com/?http#tocS_ModLoaderType + .arg(mcVersion); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); + + QObject::connect(netJob, &NetJob::succeeded, this, &FlameMod::ListModel::searchRequestFinished); QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); } -void ListModel::searchWithTerm(const QString &term, const int sort) -{ - if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { - return; - } - currentSearchTerm = term; - currentSort = sort; - if(jobPtr) { - jobPtr->abort(); - searchState = ResetRequested; - return; - } - else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; - } - nextSearchOffset = 0; - performPaginatedSearch(); -} - -void ListModel::searchRequestFinished() +void FlameMod::ListModel::searchRequestFinished() { jobPtr.reset(); @@ -253,21 +87,4 @@ void ListModel::searchRequestFinished() endInsertRows(); } -void ListModel::searchRequestFailed(QString reason) -{ - jobPtr.reset(); - - if(searchState == ResetRequested) { - beginResetModel(); - modpacks.clear(); - endResetModel(); - - nextSearchOffset = 0; - performPaginatedSearch(); - } else { - searchState = Finished; - } -} - -} - +} // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h index ec19ab2f..a585331d 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h @@ -2,78 +2,36 @@ #include -#include -#include -#include #include -#include #include +#include +#include #include #include -#include +#include +#include -#include #include +#include -#include -#include "modplatform/flame/FlameModIndex.h" #include "BaseInstance.h" #include "FlameModPage.h" +#include "modplatform/flame/FlameModIndex.h" namespace FlameMod { - -typedef QMap LogoMap; typedef std::function LogoCallback; -class ListModel : public QAbstractListModel -{ +class ListModel : public ModPlatform::ListModel { Q_OBJECT -public: - ListModel(FlameModPage *parent); + public: + ListModel(FlameModPage* parent); virtual ~ListModel(); - int rowCount(const QModelIndex &parent) const override; - int columnCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - bool canFetchMore(const QModelIndex & parent) const override; - void fetchMore(const QModelIndex & parent) override; - - void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); - void searchWithTerm(const QString &term, const int sort); - -private slots: - void performPaginatedSearch(); - - void logoFailed(QString logo); - void logoLoaded(QString logo, QIcon out); - - void searchRequestFinished(); - void searchRequestFailed(QString reason); - -private: - void requestLogo(QString file, QString url); - -private: - QList modpacks; - QStringList m_failedLogos; - QStringList m_loadingLogos; - LogoMap m_logoMap; - QMap waitingCallbacks; - - QString currentSearchTerm; - int currentSort = 0; - int nextSearchOffset = 0; - enum SearchState { - None, - CanPossiblyFetchMore, - ResetRequested, - Finished - } searchState = None; - NetJob::Ptr jobPtr; - QByteArray response; + private slots: + void performPaginatedSearch() override; + void searchRequestFinished() override; }; -} +} // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 48d38b82..ef33f972 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -1,5 +1,5 @@ #include "FlameModPage.h" -#include "ui_FlameModPage.h" +#include "ui_ModPage.h" #include @@ -12,206 +12,59 @@ #include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" -FlameModPage::FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance) - : QWidget(dialog), m_instance(instance), ui(new Ui::FlameModPage), - dialog(dialog) { - ui->setupUi(this); - connect(ui->searchButton, &QPushButton::clicked, this, - &FlameModPage::triggerSearch); - ui->searchEdit->installEventFilter(this); - listModel = new FlameMod::ListModel(this); - ui->packView->setModel(listModel); +FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) + : ModPage(dialog, instance) +{ + listModel = new FlameMod::ListModel(this); + ui->packView->setModel(listModel); - ui->versionSelectionBox->view()->setVerticalScrollBarPolicy( - Qt::ScrollBarAsNeeded); - ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + // index is used to set the sorting with the flame 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 Downloads")); - // index is used to set the sorting with the flame 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 Downloads")); - - connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, - SLOT(triggerSearch())); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, - this, &FlameModPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, - &FlameModPage::onVersionSelectionChanged); - connect(ui->modSelectionButton, &QPushButton::clicked, this, - &FlameModPage::onModSelected); -} - -FlameModPage::~FlameModPage() { delete ui; } - -bool FlameModPage::eventFilter(QObject *watched, QEvent *event) { - if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Return) { - triggerSearch(); - keyEvent->accept(); - return true; - } - } - return QWidget::eventFilter(watched, event); + // 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(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged); + connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected); } bool FlameModPage::shouldDisplay() const { return true; } -void FlameModPage::openedImpl() { - updateSelectionButton(); - triggerSearch(); -} - -void FlameModPage::triggerSearch() { - listModel->searchWithTerm(ui->searchEdit->text(), - ui->sortByBox->currentIndex()); -} - -void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) { - ui->versionSelectionBox->clear(); - - if (!first.isValid()) { - return; - } - - current = listModel->data(first, Qt::UserRole).value(); - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "" + name + ""; - if (!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) { - authorStrs.push_back(authorToStr(author)); +void FlameModPage::onModVersionSucceed(ModPage* instance, QByteArray* response, QString addonId) +{ + if (addonId != current.addonId) { + return; // wrong request } - text += "
" + tr(" by ") + authorStrs.join(", "); - } - text += "

"; - - ui->packDescription->setHtml(text + current.description); - - if (!current.versionsLoaded) { - qDebug() << "Loading flame mod versions"; - - ui->modSelectionButton->setText(tr("Loading versions...")); - ui->modSelectionButton->setEnabled(false); - - auto netJob = - new NetJob(QString("Flame::ModVersions(%1)").arg(current.name), - APPLICATION->network()); - auto response = new QByteArray(); - int addonId = current.addonId.toInt(); - netJob->addNetAction(Net::Download::makeByteArray( - QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files") - .arg(addonId), - response)); - - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] { - if(addonId != 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 Flame at " - << parse_error.offset - << " reason: " << parse_error.errorString(); + 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; return; - } - QJsonArray arr = doc.array(); - try { - FlameMod::loadIndexedPackVersions(current, arr, APPLICATION->network(), - m_instance); - } catch (const JSONValidationError &e) { + } + QJsonArray arr = doc.array(); + try { + FlameMod::loadIndexedPackVersions(current, arr, APPLICATION->network(), m_instance); + } catch (const JSONValidationError& e) { qDebug() << *response; qWarning() << "Error while reading Flame mod version: " << e.cause(); - } - auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile(); - QString mcVersion = packProfile->getComponentVersion("net.minecraft"); - QString loaderString = - (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) - ? "fabric" - : "forge"; - for (int i = 0; i < current.versions.size(); i++) { - auto version = current.versions[i]; - if (!version.mcVersion.contains(mcVersion)) { - continue; - } - ui->versionSelectionBox->addItem(version.version, QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No Valid Version found!"), - QVariant(-1)); - } - - ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); - updateSelectionButton(); - }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { - netJob->deleteLater(); - delete response; - }); - netJob->start(); - } else { + } + auto packProfile = ((MinecraftInstance*)m_instance)->getPackProfile(); + QString mcVersion = packProfile->getComponentVersion("net.minecraft"); + QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge"; for (int i = 0; i < current.versions.size(); i++) { - ui->versionSelectionBox->addItem(current.versions[i].version, - QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No Valid Version found!"), - QVariant(-1)); + auto version = current.versions[i]; + if (!version.mcVersion.contains(mcVersion)) { continue; } + ui->versionSelectionBox->addItem(version.version, QVariant(i)); } + if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No Valid Version found!"), QVariant(-1)); } + ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); updateSelectionButton(); - } -} - -void FlameModPage::updateSelectionButton() { - if (!isOpened || selectedVersion < 0) { - ui->modSelectionButton->setEnabled(false); - return; - } - - ui->modSelectionButton->setEnabled(true); - auto &version = current.versions[selectedVersion]; - if (!dialog->isModSelected(current.name, version.fileName)) { - ui->modSelectionButton->setText(tr("Select mod for download")); - } else { - ui->modSelectionButton->setText(tr("Deselect mod for download")); - } -} - -void FlameModPage::onVersionSelectionChanged(QString data) { - if (data.isNull() || data.isEmpty()) { - selectedVersion = -1; - return; - } - selectedVersion = ui->versionSelectionBox->currentData().toInt(); - updateSelectionButton(); -} - -void FlameModPage::onModSelected() { - auto &version = current.versions[selectedVersion]; - if (dialog->isModSelected(current.name, version.fileName)) { - dialog->removeSelectedMod(current.name); - } else { - dialog->addSelectedMod(current.name, - new ModDownloadTask(version.downloadUrl, - version.fileName, dialog->mods)); - } - - updateSelectionButton(); } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 24dfcbc7..2daa155f 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -1,68 +1,24 @@ #pragma once -#include +#include "ui/pages/modplatform/ModPage.h" -#include "ui/pages/BasePage.h" -#include -#include "tasks/Task.h" -#include "modplatform/flame/FlameModIndex.h" - -namespace Ui -{ -class FlameModPage; -} - -class ModDownloadDialog; - -namespace FlameMod { - class ListModel; -} - -class FlameModPage : public QWidget, public BasePage -{ +class FlameModPage : public ModPage { Q_OBJECT -public: - explicit FlameModPage(ModDownloadDialog *dialog, BaseInstance *instance); - virtual ~FlameModPage(); - virtual QString displayName() const override - { - return tr("CurseForge"); - } - virtual QIcon icon() const override - { - return APPLICATION->getThemedIcon("flame"); - } - virtual QString id() const override - { - return "curseforge"; - } - virtual QString helpPage() const override - { - return "Flame-platform"; - } - virtual bool shouldDisplay() const override; + public: + explicit FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance); + virtual ~FlameModPage() = default; - void openedImpl() override; + inline QString displayName() const override { return tr("CurseForge"); } + inline QIcon icon() const override { return APPLICATION->getThemedIcon("flame"); } + inline QString id() const override { return "curseforge"; } + inline QString helpPage() const override { return "Flame-platform"; } - bool eventFilter(QObject * watched, QEvent * event) override; + inline QString debugName() const override { return tr("Flame"); } + inline QString metaEntryBase() const override { return "FlameMods"; }; - BaseInstance *m_instance; + bool shouldDisplay() const override; -private: - void updateSelectionButton(); - -private slots: - void triggerSearch(); - void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); - void onModSelected(); - -private: - Ui::FlameModPage *ui = nullptr; - ModDownloadDialog* dialog = nullptr; - FlameMod::ListModel* listModel = nullptr; - ModPlatform::IndexedPack current; - - int selectedVersion = -1; + private: + void onModVersionSucceed(ModPage*, QByteArray*, QString) override; }; diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.ui b/launcher/ui/pages/modplatform/flame/FlameModPage.ui deleted file mode 100644 index 36df7e8a..00000000 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.ui +++ /dev/null @@ -1,97 +0,0 @@ - - - FlameModPage - - - - 0 - 0 - 837 - 685 - - - - - - - - - - - - - - - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Select mod for download - - - - - - - - - Search and filter ... - - - - - - - - - Qt::ScrollBarAlwaysOff - - - true - - - - 48 - 48 - - - - - - - - true - - - true - - - - - - - - - Search - - - - - - - searchEdit - searchButton - packView - packDescription - sortByBox - versionSelectionBox - - - - diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 088ca411..d2e86ef8 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -1,171 +1,25 @@ #include "ModrinthModel.h" -#include "Application.h" +#include "ModrinthPage.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" -#include "ModrinthPage.h" -#include "ui/dialogs/ModDownloadDialog.h" + #include -#include -#include - -#include -#include - - namespace Modrinth { -ListModel::ListModel(ModrinthPage *parent) : QAbstractListModel(parent) -{ -} +ListModel::ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent) {} -ListModel::~ListModel() -{ -} +ListModel::~ListModel() {} -int ListModel::rowCount(const QModelIndex &parent) const -{ - return modpacks.size(); -} - -int ListModel::columnCount(const QModelIndex &parent) const -{ - return 1; -} - -QVariant ListModel::data(const QModelIndex &index, int role) const -{ - int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { - return QString("INVALID INDEX %1").arg(pos); - } - - ModPlatform::IndexedPack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { - return pack.name; - } - else if (role == Qt::ToolTipRole) - { - if(pack.description.length() > 100) - { - //some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); - edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); - return edit; - - } - return pack.description; - } - else if(role == Qt::DecorationRole) - { - 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); - return icon; - } - else if(role == Qt::UserRole) - { - QVariant v; - v.setValue(pack); - return v; - } - - return QVariant(); -} - -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) { - emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); - } - } -} - -void ListModel::logoFailed(QString logo) -{ - m_failedLogos.append(logo); - m_loadingLogos.removeAll(logo); -} - -void ListModel::requestLogo(QString logo, QString url) -{ - if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) - { - return; - } - - MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0))); - auto job = new NetJob(QString("Modrinth Icon Download %1").arg(logo), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); - - auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] - { - job->deleteLater(); - emit logoLoaded(logo, QIcon(fullPath)); - if(waitingCallbacks.contains(logo)) - { - waitingCallbacks.value(logo)(fullPath); - } - }); - - QObject::connect(job, &NetJob::failed, this, [this, logo, job] - { - job->deleteLater(); - emit logoFailed(logo); - }); - - job->start(); - m_loadingLogos.append(logo); -} - -void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) -{ - if(m_logoMap.contains(logo)) - { - callback(APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); - } - else - { - requestLogo(logo, logoUrl); - } -} - -Qt::ItemFlags ListModel::flags(const QModelIndex &index) const -{ - return QAbstractListModel::flags(index); -} - -bool ListModel::canFetchMore(const QModelIndex& parent) const -{ - return searchState == CanPossiblyFetchMore; -} - -void ListModel::fetchMore(const QModelIndex& parent) -{ - if (parent.isValid()) - return; - if(nextSearchOffset == 0) { - qWarning() << "fetchMore with 0 offset is wrong..."; - return; - } - performPaginatedSearch(); -} -const char* sorts[5]{"relevance","downloads","follows","updated","newest"}; +const char* sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; void ListModel::performPaginatedSearch() { - - QString mcVersion = ((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); - bool hasFabric = !((MinecraftInstance *)((ModrinthPage *)parent())->m_instance)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); + QString mcVersion = ((MinecraftInstance*)((ModrinthPage*)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); + bool hasFabric = !((MinecraftInstance*)((ModrinthPage*)parent())->m_instance) + ->getPackProfile() + ->getComponentVersion("net.fabricmc.fabric-loader") + .isEmpty(); auto netJob = new NetJob("Modrinth::Search", APPLICATION->network()); auto searchUrl = QString( "https://api.modrinth.com/v2/search?" @@ -173,41 +27,19 @@ void ListModel::performPaginatedSearch() "limit=25&" "query=%2&" "index=%3&" - "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]" - ) - .arg(nextSearchOffset) - .arg(currentSearchTerm) - .arg(sorts[currentSort]) - .arg(hasFabric ? "fabric" : "forge") - .arg(mcVersion); + "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]") + .arg(nextSearchOffset) + .arg(currentSearchTerm) + .arg(sorts[currentSort]) + .arg(hasFabric ? "fabric" : "forge") + .arg(mcVersion); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); -} -void ListModel::searchWithTerm(const QString &term, const int sort) -{ - if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { - return; - } - currentSearchTerm = term; - currentSort = sort; - if(jobPtr) { - jobPtr->abort(); - searchState = ResetRequested; - return; - } - else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; - } - nextSearchOffset = 0; - performPaginatedSearch(); + QObject::connect(netJob, &NetJob::succeeded, this, &Modrinth::ListModel::searchRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); } void Modrinth::ListModel::searchRequestFinished() @@ -216,30 +48,28 @@ void Modrinth::ListModel::searchRequestFinished() 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(); + 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; } QList newList; auto packs = doc.object().value("hits").toArray(); - for(auto packRaw : packs) { + for (auto packRaw : packs) { auto packObj = packRaw.toObject(); ModPlatform::IndexedPack pack; - try - { + try { Modrinth::loadIndexedPack(pack, packObj); newList.append(pack); - } - catch(const JSONValidationError &e) - { + } catch (const JSONValidationError& e) { qWarning() << "Error while loading mod from Modrinth: " << e.cause(); continue; } } - if(packs.size() < 25) { + if (packs.size() < 25) { searchState = Finished; } else { nextSearchOffset += 25; @@ -250,27 +80,4 @@ void Modrinth::ListModel::searchRequestFinished() endInsertRows(); } -void Modrinth::ListModel::searchRequestFailed(QString reason) -{ - if(jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409){ - //409 Gone, notify user to update - QMessageBox::critical(nullptr, tr("Error"), tr("Modrinth API version too old!\nPlease update PolyMC!")); - //self-destruct - ((ModDownloadDialog *)((ModrinthPage *)parent())->parentWidget())->reject(); - } - jobPtr.reset(); - - if(searchState == ResetRequested) { - beginResetModel(); - modpacks.clear(); - endResetModel(); - - nextSearchOffset = 0; - performPaginatedSearch(); - } else { - searchState = Finished; - } -} - -} - +} // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 20661412..73492356 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -2,78 +2,36 @@ #include -#include -#include -#include #include -#include #include +#include +#include #include #include -#include +#include +#include -#include #include +#include -#include -#include "modplatform/modrinth/ModrinthPackIndex.h" #include "BaseInstance.h" #include "ModrinthPage.h" +#include "modplatform/modrinth/ModrinthPackIndex.h" namespace Modrinth { - -typedef QMap LogoMap; typedef std::function LogoCallback; -class ListModel : public QAbstractListModel -{ +class ListModel : public ModPlatform::ListModel { Q_OBJECT -public: - ListModel(ModrinthPage *parent); + public: + ListModel(ModrinthPage* parent); virtual ~ListModel(); - int rowCount(const QModelIndex &parent) const override; - int columnCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - bool canFetchMore(const QModelIndex & parent) const override; - void fetchMore(const QModelIndex & parent) override; - - void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); - void searchWithTerm(const QString &term, const int sort); - -private slots: - void performPaginatedSearch(); - - void logoFailed(QString logo); - void logoLoaded(QString logo, QIcon out); - - void searchRequestFinished(); - void searchRequestFailed(QString reason); - -private: - void requestLogo(QString file, QString url); - -private: - QList modpacks; - QStringList m_failedLogos; - QStringList m_loadingLogos; - LogoMap m_logoMap; - QMap waitingCallbacks; - - QString currentSearchTerm; - int currentSort = 0; - int nextSearchOffset = 0; - enum SearchState { - None, - CanPossiblyFetchMore, - ResetRequested, - Finished - } searchState = None; - NetJob::Ptr jobPtr; - QByteArray response; + private slots: + void performPaginatedSearch() override; + void searchRequestFinished() override; }; -} +} // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 4dfc8504..aa3efe55 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -1,5 +1,5 @@ #include "ModrinthPage.h" -#include "ui_ModrinthPage.h" +#include "ui_ModPage.h" #include @@ -12,194 +12,57 @@ #include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" -ModrinthPage::ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance) - : QWidget(dialog), m_instance(instance), ui(new Ui::ModrinthPage), - dialog(dialog) { - ui->setupUi(this); - connect(ui->searchButton, &QPushButton::clicked, this, - &ModrinthPage::triggerSearch); - ui->searchEdit->installEventFilter(this); - listModel = new Modrinth::ListModel(this); - ui->packView->setModel(listModel); +ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) + : ModPage(dialog, instance) +{ + listModel = new Modrinth::ListModel(this); + ui->packView->setModel(listModel); - ui->versionSelectionBox->view()->setVerticalScrollBarPolicy( - Qt::ScrollBarAsNeeded); - ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + // index is used to set the sorting with the modrinth api + ui->sortByBox->addItem(tr("Sort by Relevence")); + ui->sortByBox->addItem(tr("Sort by Downloads")); + ui->sortByBox->addItem(tr("Sort by Follows")); + ui->sortByBox->addItem(tr("Sort by last updated")); + ui->sortByBox->addItem(tr("Sort by newest")); - // index is used to set the sorting with the modrinth api - ui->sortByBox->addItem(tr("Sort by Relevence")); - ui->sortByBox->addItem(tr("Sort by Downloads")); - ui->sortByBox->addItem(tr("Sort by Follows")); - ui->sortByBox->addItem(tr("Sort by last updated")); - ui->sortByBox->addItem(tr("Sort by newest")); - - connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, - SLOT(triggerSearch())); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, - this, &ModrinthPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, - &ModrinthPage::onVersionSelectionChanged); - connect(ui->modSelectionButton, &QPushButton::clicked, this, - &ModrinthPage::onModSelected); -} - -ModrinthPage::~ModrinthPage() { delete ui; } - -bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) { - if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { - QKeyEvent *keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Return) { - triggerSearch(); - keyEvent->accept(); - return true; - } - } - return QWidget::eventFilter(watched, event); + // 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(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); + connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthPage::onModSelected); } bool ModrinthPage::shouldDisplay() const { return true; } -void ModrinthPage::openedImpl() { - updateSelectionButton(); - triggerSearch(); -} - -void ModrinthPage::triggerSearch() { - listModel->searchWithTerm(ui->searchEdit->text(), - ui->sortByBox->currentIndex()); -} - -void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { - ui->versionSelectionBox->clear(); - - if (!first.isValid()) { - return; - } - - current = listModel->data(first, Qt::UserRole).value(); - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "" + name + ""; - text += "
" + tr(" by ") + "" + - current.authors[0].name + "

"; - ui->packDescription->setHtml(text + current.description); - - if (!current.versionsLoaded) { - qDebug() << "Loading Modrinth mod versions"; - - ui->modSelectionButton->setText(tr("Loading versions...")); - ui->modSelectionButton->setEnabled(false); - - auto netJob = - new NetJob(QString("Modrinth::ModVersions(%1)").arg(current.name), - APPLICATION->network()); - auto response = new QByteArray(); - QString addonId = current.addonId.toString(); - netJob->addNetAction(Net::Download::makeByteArray( - QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId), - response)); - - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] { - if(addonId != current.addonId){ - return; - } - 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 +void ModrinthPage::onModVersionSucceed(ModPage* instance, QByteArray* response, QString addonId) +{ + if (addonId != current.addonId) { return; } + 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; - } - QJsonArray arr = doc.array(); - try { - Modrinth::loadIndexedPackVersions(current, arr, APPLICATION->network(), - m_instance); - } catch (const JSONValidationError &e) { + } + QJsonArray arr = doc.array(); + try { + Modrinth::loadIndexedPackVersions(current, arr, APPLICATION->network(), m_instance); + } catch (const JSONValidationError& e) { qDebug() << *response; qWarning() << "Error while reading Modrinth mod version: " << e.cause(); - } - auto packProfile = ((MinecraftInstance *)m_instance)->getPackProfile(); - QString mcVersion = packProfile->getComponentVersion("net.minecraft"); - QString loaderString = - (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) - ? "fabric" - : "forge"; - for (int i = 0; i < current.versions.size(); i++) { - auto version = current.versions[i]; - if (!version.mcVersion.contains(mcVersion) || - !version.loaders.contains(loaderString)) { - continue; - } - ui->versionSelectionBox->addItem(version.version, QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No Valid Version found !"), - QVariant(-1)); - } - - ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); - updateSelectionButton(); - }); - - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { - netJob->deleteLater(); - delete response; - }); - - netJob->start(); - } else { + } + auto packProfile = ((MinecraftInstance*)m_instance)->getPackProfile(); + QString mcVersion = packProfile->getComponentVersion("net.minecraft"); + QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge"; for (int i = 0; i < current.versions.size(); i++) { - ui->versionSelectionBox->addItem(current.versions[i].version, - QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No Valid Version found !"), - QVariant(-1)); + auto version = current.versions[i]; + if (!version.mcVersion.contains(mcVersion) || !version.loaders.contains(loaderString)) { continue; } + ui->versionSelectionBox->addItem(version.version, QVariant(i)); } + if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No Valid Version found !"), QVariant(-1)); } + ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); updateSelectionButton(); - } -} - -void ModrinthPage::updateSelectionButton() { - if (!isOpened || selectedVersion < 0) { - ui->modSelectionButton->setEnabled(false); - return; - } - - ui->modSelectionButton->setEnabled(true); - auto &version = current.versions[selectedVersion]; - if (!dialog->isModSelected(current.name, version.fileName)) { - ui->modSelectionButton->setText(tr("Select mod for download")); - } else { - ui->modSelectionButton->setText(tr("Deselect mod for download")); - } -} - -void ModrinthPage::onVersionSelectionChanged(QString data) { - if (data.isNull() || data.isEmpty()) { - selectedVersion = -1; - return; - } - selectedVersion = ui->versionSelectionBox->currentData().toInt(); - updateSelectionButton(); -} - -void ModrinthPage::onModSelected() { - auto &version = current.versions[selectedVersion]; - if (dialog->isModSelected(current.name, version.fileName)) { - dialog->removeSelectedMod(current.name); - } else { - dialog->addSelectedMod(current.name, - new ModDownloadTask(version.downloadUrl, - version.fileName, dialog->mods)); - } - - updateSelectionButton(); } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 1d725d47..41c6c31d 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -1,68 +1,24 @@ #pragma once -#include +#include "ui/pages/modplatform/ModPage.h" -#include "ui/pages/BasePage.h" -#include -#include "tasks/Task.h" -#include "modplatform/modrinth/ModrinthPackIndex.h" - -namespace Ui -{ -class ModrinthPage; -} - -class ModDownloadDialog; - -namespace Modrinth { - class ListModel; -} - -class ModrinthPage : public QWidget, public BasePage -{ +class ModrinthPage : public ModPage { Q_OBJECT -public: - explicit ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance); - virtual ~ModrinthPage(); - virtual QString displayName() const override - { - return tr("Modrinth"); - } - virtual QIcon icon() const override - { - return APPLICATION->getThemedIcon("modrinth"); - } - virtual QString id() const override - { - return "modrinth"; - } - virtual QString helpPage() const override - { - return "Modrinth-platform"; - } - virtual bool shouldDisplay() const override; + public: + explicit ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance); + virtual ~ModrinthPage() = default; - void openedImpl() override; + inline QString displayName() const override { return tr("Modrinth"); } + inline QIcon icon() const override { return APPLICATION->getThemedIcon("modrinth"); } + inline QString id() const override { return "modrinth"; } + inline QString helpPage() const override { return "Modrinth-platform"; } - bool eventFilter(QObject * watched, QEvent * event) override; + inline QString debugName() const override { return tr("Modrinth"); } + inline QString metaEntryBase() const override { return "ModrinthPacks"; }; - BaseInstance *m_instance; + bool shouldDisplay() const override; -private: - void updateSelectionButton(); - -private slots: - void triggerSearch(); - void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); - void onModSelected(); - -private: - Ui::ModrinthPage *ui = nullptr; - ModDownloadDialog* dialog = nullptr; - Modrinth::ListModel* listModel = nullptr; - ModPlatform::IndexedPack current; - - int selectedVersion = -1; + private: + void onModVersionSucceed(ModPage*, QByteArray*, QString) override; }; From 2d68308d4920be4c28a73d7646814765eee7b7a2 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 2 Mar 2022 23:01:23 -0300 Subject: [PATCH 051/605] refactor: move url creation for mods to modplatform/ Moves all things related to creating the URLs of the mod platforms that go to network tasks to a single place, so that: 1. Maintaining and fixing eventual issues is more straightforward. 2. Makes it possible to factor out more common code between the different modplatform pages --- launcher/modplatform/ModAPI.h | 12 ++++++ launcher/modplatform/flame/FlameAPI.h | 27 ++++++++++++ launcher/modplatform/modrinth/ModrinthAPI.h | 25 +++++++++++ .../modrinth/ModrinthPackIndex.cpp | 5 ++- launcher/ui/pages/modplatform/ModModel.cpp | 20 +++++++++ launcher/ui/pages/modplatform/ModModel.h | 7 ++- launcher/ui/pages/modplatform/ModPage.cpp | 9 +--- launcher/ui/pages/modplatform/ModPage.h | 2 + .../pages/modplatform/flame/FlameModModel.cpp | 43 +++---------------- .../pages/modplatform/flame/FlameModModel.h | 4 +- .../ui/pages/modplatform/flame/FlameModPage.h | 6 +++ .../modplatform/modrinth/ModrinthModel.cpp | 39 +++-------------- .../modplatform/modrinth/ModrinthModel.h | 4 +- .../pages/modplatform/modrinth/ModrinthPage.h | 6 +++ 14 files changed, 130 insertions(+), 79 deletions(-) create mode 100644 launcher/modplatform/ModAPI.h create mode 100644 launcher/modplatform/flame/FlameAPI.h create mode 100644 launcher/modplatform/modrinth/ModrinthAPI.h diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h new file mode 100644 index 00000000..e60fa8e0 --- /dev/null +++ b/launcher/modplatform/ModAPI.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +class ModAPI { + public: + virtual ~ModAPI() = default; + + inline virtual QString getModSearchURL(int, QString, QString, bool, QString) const { return ""; }; + inline virtual QString getVersionsURL(const QString& addonId) const { return ""; }; + inline virtual QString getAuthorURL(const QString& name) const { return ""; }; +}; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h new file mode 100644 index 00000000..6e2b9e25 --- /dev/null +++ b/launcher/modplatform/flame/FlameAPI.h @@ -0,0 +1,27 @@ +#pragma once + +#include "modplatform/ModAPI.h" + +class FlameAPI : public ModAPI { + public: + inline QString getModSearchURL(int index, QString searchFilter, QString sort, bool fabricCompatible, QString version) const override + { + return QString("https://addons-ecs.forgesvc.net/api/v2/addon/search?" + "gameId=432&" "categoryId=0&" "sectionId=6&" + + "index=%1&" "pageSize=25&" "searchFilter=%2&" + "sort=%3&" "modLoaderType=%4&" "gameVersion=%5") + .arg(index) + .arg(searchFilter) + .arg(sort) + .arg(fabricCompatible ? 4 : 1) // Enum: https://docs.curseforge.com/?http#tocS_ModLoaderType + .arg(version); + }; + + inline QString getVersionsURL(const QString& addonId) const override + { + return QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId); + }; + + inline QString getAuthorURL(const QString& name) const override { return ""; }; +}; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h new file mode 100644 index 00000000..4ae8b8f9 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -0,0 +1,25 @@ +#pragma once + +#include "modplatform/ModAPI.h" + +class ModrinthAPI : public ModAPI { + public: + inline QString getModSearchURL(int offset, QString query, QString sort, bool fabricCompatible, QString version) const override + { + return QString("https://api.modrinth.com/v2/search?" + "offset=%1&" "limit=25&" "query=%2&" "index=%3&" + "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]") + .arg(offset) + .arg(query) + .arg(sort) + .arg(fabricCompatible ? "fabric" : "forge") + .arg(version); + }; + + inline QString getVersionsURL(const QString& addonId) const override + { + return QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId); + }; + + inline QString getAuthorURL(const QString& name) const override { return "https://modrinth.com/user/" + name; }; +}; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index a59186f7..02aac34d 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -1,10 +1,13 @@ #include "ModrinthPackIndex.h" +#include "ModrinthAPI.h" #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "net/NetJob.h" +static ModrinthAPI api; + void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireString(obj, "project_id"); @@ -17,7 +20,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) ModPlatform::ModpackAuthor modAuthor; modAuthor.name = Json::requireString(obj, "author"); - modAuthor.url = "https://modrinth.com/user/" + modAuthor.name; + modAuthor.url = api.getAuthorURL(modAuthor.name); pack.authors.append(modAuthor); } diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 5bd7e33e..481f1c56 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -1,6 +1,8 @@ #include "ModModel.h" #include "ModPage.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" #include @@ -95,6 +97,24 @@ void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallbac } } +void ListModel::performPaginatedSearch() +{ + QString mcVersion = ((MinecraftInstance*)((ModPage*)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); + bool hasFabric = !((MinecraftInstance*)((ModPage*)parent())->m_instance) + ->getPackProfile() + ->getComponentVersion("net.fabricmc.fabric-loader") + .isEmpty(); + auto netJob = new NetJob(QString("%1::Search").arg(m_parent->debugName()), APPLICATION->network()); + auto searchUrl = m_parent->apiProvider()->getModSearchURL(nextSearchOffset, currentSearchTerm, getSorts()[currentSort], hasFabric, mcVersion); + + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(); + + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); +} + void ListModel::searchWithTerm(const QString& term, const int sort) { if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { return; } diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index ae8ceb7c..23e544f1 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -3,6 +3,7 @@ #include #include "modplatform/ModIndex.h" +#include "modplatform/ModAPI.h" #include "net/NetJob.h" class ModPage; @@ -30,15 +31,18 @@ class ListModel : public QAbstractListModel { void searchWithTerm(const QString& term, const int sort); protected slots: - virtual void performPaginatedSearch() = 0; virtual void searchRequestFinished() = 0; + void performPaginatedSearch(); + void logoFailed(QString logo); void logoLoaded(QString logo, QIcon out); void searchRequestFailed(QString reason); protected: + virtual const char** getSorts() const = 0; + void requestLogo(QString file, QString url); protected: @@ -56,5 +60,6 @@ class ListModel : public QAbstractListModel { enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; NetJob::Ptr jobPtr; QByteArray response; + }; } // namespace ModPlatform diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 8af534a6..2d47e31c 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -101,13 +101,8 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(debugName()).arg(current.name), APPLICATION->network()); auto response = new QByteArray(); QString addonId = current.addonId.toString(); - //FIXME - if(debugName() == "Modrinth") - netJob->addNetAction( - Net::Download::makeByteArray(QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId), response)); - else - netJob->addNetAction( - Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response)); + + netJob->addNetAction(Net::Download::makeByteArray(apiProvider()->getVersionsURL(addonId), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId]{ onModVersionSucceed(this, response, addonId); diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index c0102549..4657bc5e 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -3,6 +3,7 @@ #include #include +#include "modplatform/ModAPI.h" #include "modplatform/ModIndex.h" #include "tasks/Task.h" #include "ui/pages/BasePage.h" @@ -30,6 +31,7 @@ class ModPage : public QWidget, public BasePage { inline virtual QString metaEntryBase() const = 0; virtual bool shouldDisplay() const override = 0; + virtual const ModAPI* apiProvider() const = 0; void openedImpl() override; bool eventFilter(QObject* watched, QEvent* event) override; diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index 5ab6672f..283f9ce7 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -1,6 +1,5 @@ #include "FlameModModel.h" #include "FlameModPage.h" -#include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include @@ -11,41 +10,6 @@ ListModel::ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {} ListModel::~ListModel() {} -const char* sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; - -void ListModel::performPaginatedSearch() -{ - QString mcVersion = ((MinecraftInstance*)((FlameModPage*)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); - bool hasFabric = !((MinecraftInstance*)((FlameModPage*)parent())->m_instance) - ->getPackProfile() - ->getComponentVersion("net.fabricmc.fabric-loader") - .isEmpty(); - auto netJob = new NetJob("Flame::Search", APPLICATION->network()); - auto searchUrl = QString( - "https://addons-ecs.forgesvc.net/api/v2/addon/search?" - "gameId=432&" - "categoryId=0&" - "sectionId=6&" - - "index=%1&" - "pageSize=25&" - "searchFilter=%2&" - "sort=%3&" - "modLoaderType=%4&" - "gameVersion=%5") - .arg(nextSearchOffset) - .arg(currentSearchTerm) - .arg(sorts[currentSort]) - .arg(hasFabric ? 4 : 1) // Enum: https://docs.curseforge.com/?http#tocS_ModLoaderType - .arg(mcVersion); - - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); - jobPtr = netJob; - jobPtr->start(); - - QObject::connect(netJob, &NetJob::succeeded, this, &FlameMod::ListModel::searchRequestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); -} void FlameMod::ListModel::searchRequestFinished() { @@ -87,4 +51,11 @@ void FlameMod::ListModel::searchRequestFinished() endInsertRows(); } +const char* sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; + +const char** FlameMod::ListModel::getSorts() const +{ + return sorts; +} + } // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h index a585331d..ae919e63 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h @@ -30,8 +30,10 @@ class ListModel : public ModPlatform::ListModel { virtual ~ListModel(); private slots: - void performPaginatedSearch() override; void searchRequestFinished() override; + + private: + const char** getSorts() const override; }; } // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 2daa155f..e7d98cb0 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -2,6 +2,8 @@ #include "ui/pages/modplatform/ModPage.h" +#include "modplatform/flame/FlameAPI.h" + class FlameModPage : public ModPage { Q_OBJECT @@ -18,7 +20,11 @@ class FlameModPage : public ModPage { inline QString metaEntryBase() const override { return "FlameMods"; }; bool shouldDisplay() const override; + const ModAPI* apiProvider() const override { return &api; }; private: void onModVersionSucceed(ModPage*, QByteArray*, QString) override; + + private: + FlameAPI api; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index d2e86ef8..dc3d1469 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -1,7 +1,6 @@ #include "ModrinthModel.h" #include "ModrinthPage.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" #include @@ -11,37 +10,6 @@ ListModel::ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent) {} ListModel::~ListModel() {} -const char* sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; - -void ListModel::performPaginatedSearch() -{ - QString mcVersion = ((MinecraftInstance*)((ModrinthPage*)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); - bool hasFabric = !((MinecraftInstance*)((ModrinthPage*)parent())->m_instance) - ->getPackProfile() - ->getComponentVersion("net.fabricmc.fabric-loader") - .isEmpty(); - auto netJob = new NetJob("Modrinth::Search", APPLICATION->network()); - auto searchUrl = QString( - "https://api.modrinth.com/v2/search?" - "offset=%1&" - "limit=25&" - "query=%2&" - "index=%3&" - "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]") - .arg(nextSearchOffset) - .arg(currentSearchTerm) - .arg(sorts[currentSort]) - .arg(hasFabric ? "fabric" : "forge") - .arg(mcVersion); - - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); - jobPtr = netJob; - jobPtr->start(); - - QObject::connect(netJob, &NetJob::succeeded, this, &Modrinth::ListModel::searchRequestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); -} - void Modrinth::ListModel::searchRequestFinished() { jobPtr.reset(); @@ -80,4 +48,11 @@ void Modrinth::ListModel::searchRequestFinished() endInsertRows(); } +const char* sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; + +const char** Modrinth::ListModel::getSorts() const +{ + return sorts; +} + } // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 73492356..b8937b50 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -30,8 +30,10 @@ class ListModel : public ModPlatform::ListModel { virtual ~ListModel(); private slots: - void performPaginatedSearch() override; void searchRequestFinished() override; + + private: + const char** getSorts() const override; }; } // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 41c6c31d..504f42ad 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -2,6 +2,8 @@ #include "ui/pages/modplatform/ModPage.h" +#include "modplatform/modrinth/ModrinthAPI.h" + class ModrinthPage : public ModPage { Q_OBJECT @@ -18,7 +20,11 @@ class ModrinthPage : public ModPage { inline QString metaEntryBase() const override { return "ModrinthPacks"; }; bool shouldDisplay() const override; + const ModAPI* apiProvider() const override { return &api; }; private: void onModVersionSucceed(ModPage*, QByteArray*, QString) override; + + private: + ModrinthAPI api; }; From 9a8599e4ba7e1df616d0e4b4a4a899792ecaa3e1 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 3 Mar 2022 00:06:37 -0300 Subject: [PATCH 052/605] fix windows compilation --- launcher/ui/pages/modplatform/ModPage.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 4657bc5e..f79d494f 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -22,13 +22,13 @@ class ModPage : public QWidget, public BasePage { explicit ModPage(ModDownloadDialog* dialog, BaseInstance* instance); virtual ~ModPage(); - inline virtual QString displayName() const override = 0; - inline virtual QIcon icon() const override = 0; - inline virtual QString id() const override = 0; - inline virtual QString helpPage() const override = 0; + virtual QString displayName() const override = 0; + virtual QIcon icon() const override = 0; + virtual QString id() const override = 0; + virtual QString helpPage() const override = 0; - inline virtual QString debugName() const = 0; - inline virtual QString metaEntryBase() const = 0; + virtual QString debugName() const = 0; + virtual QString metaEntryBase() const = 0; virtual bool shouldDisplay() const override = 0; virtual const ModAPI* apiProvider() const = 0; From f95cebaf06e30705d9980fa765189674f13113a7 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 3 Mar 2022 01:10:10 -0300 Subject: [PATCH 053/605] change 'Install Mods' -> 'Download Mods' and change position --- launcher/ui/pages/instance/ModFolderPage.cpp | 3 +-- launcher/ui/pages/instance/ModFolderPage.ui | 9 +++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index b342accf..3baec1de 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -144,8 +144,7 @@ ModFolderPage::ModFolderPage( { ui->setupUi(this); if(id == "mods") { - auto act = new QAction(tr("Install Mods"), this); - ui->actionsToolbar->insertActionBefore(ui->actionView_configs,act); + auto act = ui->actionDownload; connect(act, &QAction::triggered, this, &ModFolderPage::on_actionInstall_mods_triggered); } ui->actionsToolbar->insertSpacer(ui->actionView_configs); diff --git a/launcher/ui/pages/instance/ModFolderPage.ui b/launcher/ui/pages/instance/ModFolderPage.ui index 0fb51e84..75e0d1c5 100644 --- a/launcher/ui/pages/instance/ModFolderPage.ui +++ b/launcher/ui/pages/instance/ModFolderPage.ui @@ -84,6 +84,7 @@ false + @@ -99,6 +100,14 @@ Add mods
+ + + &Download Mods + + + Download Mods + + &Remove From 9e443faba3fe8ef9b14fd466e9e98aaeaf68656e Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 3 Mar 2022 04:02:22 -0300 Subject: [PATCH 054/605] hack: hide 'Download Mods' button when not in the mods tab --- launcher/ui/pages/instance/ModFolderPage.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 3baec1de..3becd1db 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -143,10 +143,17 @@ ModFolderPage::ModFolderPage( ui(new Ui::ModFolderPage) { ui->setupUi(this); + auto act = ui->actionDownload; if(id == "mods") { - auto act = ui->actionDownload; connect(act, &QAction::triggered, this, &ModFolderPage::on_actionInstall_mods_triggered); } + else{ + // HACK: Prevent the download button from showing in the shaders / resource packs tab + // This whole thing needs some cleaning up anyway, no next time we can do it properly... + act->setVisible(false); + act->setText(""); + act->setToolTip(""); + } ui->actionsToolbar->insertSpacer(ui->actionView_configs); m_inst = inst; From e0c025b162fe44fb29951d4bd55e675ae504c871 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 3 Mar 2022 09:51:46 -0300 Subject: [PATCH 055/605] fix extra spacing in resource packs and shader packs, and move button up hopefully now its finally ok --- launcher/ui/pages/instance/ModFolderPage.cpp | 18 ++++++++++-------- launcher/ui/pages/instance/ModFolderPage.ui | 11 +---------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 3becd1db..e4ad9012 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -143,17 +143,19 @@ ModFolderPage::ModFolderPage( ui(new Ui::ModFolderPage) { ui->setupUi(this); - auto act = ui->actionDownload; + + // This is structured like that so that these changes + // do not affect the Resouce pack and Shader pack tabs if(id == "mods") { + auto act = new QAction(tr("Download mods"), this); + act->setToolTip(tr("Download mods from online mod platforms")); + ui->actionsToolbar->insertActionBefore(ui->actionAdd, act); connect(act, &QAction::triggered, this, &ModFolderPage::on_actionInstall_mods_triggered); + + ui->actionAdd->setText("Add .jar"); + ui->actionAdd->setToolTip("Add mods via local file"); } - else{ - // HACK: Prevent the download button from showing in the shaders / resource packs tab - // This whole thing needs some cleaning up anyway, no next time we can do it properly... - act->setVisible(false); - act->setText(""); - act->setToolTip(""); - } + ui->actionsToolbar->insertSpacer(ui->actionView_configs); m_inst = inst; diff --git a/launcher/ui/pages/instance/ModFolderPage.ui b/launcher/ui/pages/instance/ModFolderPage.ui index 75e0d1c5..ab59b0df 100644 --- a/launcher/ui/pages/instance/ModFolderPage.ui +++ b/launcher/ui/pages/instance/ModFolderPage.ui @@ -84,7 +84,6 @@ false - @@ -97,15 +96,7 @@ &Add - Add mods - - - - - &Download Mods - - - Download Mods + Add From 3697d70b48ec2c54d1574fdb0b051c61c2430f51 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 5 Mar 2022 20:29:54 +0100 Subject: [PATCH 056/605] fix: reorganize icon themes Rename MultiMC to Legacy Simple (Colored) is now the first icon theme Custom is now the last icon theme, which also fixes a loading issue when Legacy was selected Fix loading of Legacy theme --- launcher/ui/pages/global/LauncherPage.cpp | 16 ++++++++-------- launcher/ui/pages/global/LauncherPage.ui | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 0ffe8050..cec496dc 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -247,16 +247,16 @@ void LauncherPage::applySettings() switch (ui->themeComboBox->currentIndex()) { case 0: - s->set("IconTheme", "pe_dark"); + s->set("IconTheme", "pe_colored"); break; case 1: s->set("IconTheme", "pe_light"); break; case 2: - s->set("IconTheme", "pe_blue"); + s->set("IconTheme", "pe_dark"); break; case 3: - s->set("IconTheme", "pe_colored"); + s->set("IconTheme", "pe_blue"); break; case 4: s->set("IconTheme", "OSX"); @@ -268,10 +268,10 @@ void LauncherPage::applySettings() s->set("IconTheme", "flat"); break; case 7: - s->set("IconTheme", "custom"); + s->set("IconTheme", "multimc"); break; case 8: - s->set("IconTheme", "multimc"); + s->set("IconTheme", "custom"); break; } @@ -324,7 +324,7 @@ void LauncherPage::loadSettings() m_currentUpdateChannel = s->get("UpdateChannel").toString(); //FIXME: make generic auto theme = s->get("IconTheme").toString(); - if (theme == "pe_dark") + if (theme == "pe_colored") { ui->themeComboBox->setCurrentIndex(0); } @@ -332,11 +332,11 @@ void LauncherPage::loadSettings() { ui->themeComboBox->setCurrentIndex(1); } - else if (theme == "pe_blue") + else if (theme == "pe_dark") { ui->themeComboBox->setCurrentIndex(2); } - else if (theme == "pe_colored") + else if (theme == "pe_blue") { ui->themeComboBox->setCurrentIndex(3); } diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 47fed873..59096a28 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -264,7 +264,7 @@ - Simple (Dark Icons) + Simple (Colored Icons) @@ -274,12 +274,12 @@ - Simple (Blue Icons) + Simple (Dark Icons) - Simple (Colored Icons) + Simple (Blue Icons) @@ -294,7 +294,12 @@ - Flat + Flat + + + + + Legacy @@ -302,11 +307,6 @@ Custom - - - MultiMC - -
From b162351ff45a124b9f1df7658f4cfb8d607737a3 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 5 Mar 2022 21:49:13 +0100 Subject: [PATCH 057/605] fix: switch to polymc.org wiki --- launcher/ui/widgets/PageContainer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 6de49467..368bfe40 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -207,7 +207,7 @@ void PageContainer::help() QString pageId = m_currentPage->helpPage(); if (pageId.isEmpty()) return; - DesktopServices::openUrl(QUrl("https://github.com/PolyMC/PolyMC/wiki/" + pageId)); + DesktopServices::openUrl(QUrl("https://polymc.org/wiki/" + pageId)); } } From 6545d250e8f3e10385cbf86f0d7f46feb7b2456c Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 6 Mar 2022 11:31:50 +0100 Subject: [PATCH 058/605] refactor: move help URL into buildconfig --- CMakeLists.txt | 1 + buildconfig/BuildConfig.cpp.in | 1 + buildconfig/BuildConfig.h | 5 +++++ launcher/ui/widgets/PageContainer.cpp | 3 ++- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c370b6ea..bf1b222b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,7 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") ######## Set URLs ######## set(Launcher_NEWS_RSS_URL "https://polymc.github.io/feed/feed.xml" CACHE STRING "URL to fetch PolyMC's news RSS feed from.") set(Launcher_NEWS_OPEN_URL "https://polymc.github.io/news/" CACHE STRING "URL that gets opened when the user clicks 'More News'") +set(Launcher_HELP_URL "https://polymc.org/wiki/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 1) diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 0ffc9326..80c67037 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -50,6 +50,7 @@ Config::Config() VERSION_STR = "@Launcher_VERSION_STRING@"; NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@"; NEWS_OPEN_URL = "@Launcher_NEWS_OPEN_URL@"; + HELP_URL = "@Launcher_HELP_URL@"; IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@"; MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@"; META_URL = "@Launcher_META_URL@"; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 863f88df..95619587 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -73,6 +73,11 @@ public: */ QString NEWS_OPEN_URL; + /** + * URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help + */ + QString HELP_URL; + /** * Client ID you can get from Imgur when you register an application */ diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 368bfe40..558a98fb 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -14,6 +14,7 @@ */ #include "PageContainer.h" +#include "BuildConfig.h" #include "PageContainer_p.h" #include @@ -207,7 +208,7 @@ void PageContainer::help() QString pageId = m_currentPage->helpPage(); if (pageId.isEmpty()) return; - DesktopServices::openUrl(QUrl("https://polymc.org/wiki/" + pageId)); + DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg(pageId))); } } From b93daf1fb743f09d48595f554d93a49fe448d6b7 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 6 Mar 2022 11:32:06 +0100 Subject: [PATCH 059/605] fix: update news links to point to polymc.org --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bf1b222b..e2545ba5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,8 +46,8 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") ##################################### Set Application options ##################################### ######## Set URLs ######## -set(Launcher_NEWS_RSS_URL "https://polymc.github.io/feed/feed.xml" CACHE STRING "URL to fetch PolyMC's news RSS feed from.") -set(Launcher_NEWS_OPEN_URL "https://polymc.github.io/news/" CACHE STRING "URL that gets opened when the user clicks 'More News'") +set(Launcher_NEWS_RSS_URL "https://polymc.org/feed/feed.xml" CACHE STRING "URL to fetch PolyMC's news RSS feed from.") +set(Launcher_NEWS_OPEN_URL "https://polymc.org/news" CACHE STRING "URL that gets opened when the user clicks 'More News'") set(Launcher_HELP_URL "https://polymc.org/wiki/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") ######## Set version numbers ######## From ae39d16c11d5c4bc5d10e3cc87d5e55367ef2f9b Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 6 Mar 2022 11:40:11 +0100 Subject: [PATCH 060/605] chore: bump to 1.1.0 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c370b6ea..d567af80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,8 +51,8 @@ set(Launcher_NEWS_OPEN_URL "https://polymc.github.io/news/" CACHE STRING "URL th ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 1) -set(Launcher_VERSION_MINOR 0) -set(Launcher_VERSION_HOTFIX 6) +set(Launcher_VERSION_MINOR 1) +set(Launcher_VERSION_HOTFIX 0) # Build number set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") From 5e9d49a91082c53907db84df809d21c3bdc8bcac Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 6 Mar 2022 13:54:05 -0300 Subject: [PATCH 061/605] refactor: use only a single unique_ptr for the api --- launcher/ui/pages/modplatform/ModPage.cpp | 4 ++-- launcher/ui/pages/modplatform/ModPage.h | 12 +++++++++--- launcher/ui/pages/modplatform/flame/FlameModPage.cpp | 2 +- launcher/ui/pages/modplatform/flame/FlameModPage.h | 4 ---- .../ui/pages/modplatform/modrinth/ModrinthPage.cpp | 2 +- .../ui/pages/modplatform/modrinth/ModrinthPage.h | 4 ---- 6 files changed, 13 insertions(+), 15 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 2d47e31c..f427d32a 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -5,8 +5,8 @@ #include "ui/dialogs/ModDownloadDialog.h" -ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance) - : QWidget(dialog), m_instance(instance), ui(new Ui::ModPage), dialog(dialog) +ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) + : QWidget(dialog), m_instance(instance), ui(new Ui::ModPage), dialog(dialog), api(api) { ui->setupUi(this); connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index f79d494f..cd034a3a 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -15,23 +15,27 @@ namespace Ui { class ModPage; } +/* This page handles most logic related to browsing and selecting mods to download. + * By default, the methods provided work with net requests, to fetch data from remote APIs. */ class ModPage : public QWidget, public BasePage { Q_OBJECT public: - explicit ModPage(ModDownloadDialog* dialog, BaseInstance* instance); + explicit ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api); virtual ~ModPage(); + /* The name visible to the user */ virtual QString displayName() const override = 0; virtual QIcon icon() const override = 0; virtual QString id() const override = 0; virtual QString helpPage() const override = 0; - virtual QString debugName() const = 0; virtual QString metaEntryBase() const = 0; + /* This only appears in the debug console */ + virtual QString debugName() const = 0; virtual bool shouldDisplay() const override = 0; - virtual const ModAPI* apiProvider() const = 0; + const ModAPI* apiProvider() const { return api.get(); }; void openedImpl() override; bool eventFilter(QObject* watched, QEvent* event) override; @@ -55,5 +59,7 @@ class ModPage : public QWidget, public BasePage { ModPlatform::ListModel* listModel = nullptr; ModPlatform::IndexedPack current; + std::unique_ptr api; + int selectedVersion = -1; }; diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index ef33f972..47a79bd6 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -13,7 +13,7 @@ #include "ui/dialogs/ModDownloadDialog.h" FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) - : ModPage(dialog, instance) + : ModPage(dialog, instance, new FlameAPI()) { listModel = new FlameMod::ListModel(this); ui->packView->setModel(listModel); diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index e7d98cb0..89311e7f 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -20,11 +20,7 @@ class FlameModPage : public ModPage { inline QString metaEntryBase() const override { return "FlameMods"; }; bool shouldDisplay() const override; - const ModAPI* apiProvider() const override { return &api; }; private: void onModVersionSucceed(ModPage*, QByteArray*, QString) override; - - private: - FlameAPI api; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index aa3efe55..4e4a9db4 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -13,7 +13,7 @@ #include "ui/dialogs/ModDownloadDialog.h" ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) - : ModPage(dialog, instance) + : ModPage(dialog, instance, new ModrinthAPI()) { listModel = new Modrinth::ListModel(this); ui->packView->setModel(listModel); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 504f42ad..d92274dd 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -20,11 +20,7 @@ class ModrinthPage : public ModPage { inline QString metaEntryBase() const override { return "ModrinthPacks"; }; bool shouldDisplay() const override; - const ModAPI* apiProvider() const override { return &api; }; private: void onModVersionSucceed(ModPage*, QByteArray*, QString) override; - - private: - ModrinthAPI api; }; From 5a638fa97711231638615f920462ed5f5f4507e0 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 6 Mar 2022 15:23:00 -0300 Subject: [PATCH 062/605] refactor: move "get versions" task from page to model This seems more reasonable --- launcher/ui/pages/modplatform/ModModel.cpp | 20 +++++++++++++++++++ launcher/ui/pages/modplatform/ModModel.h | 2 ++ launcher/ui/pages/modplatform/ModPage.cpp | 17 +--------------- launcher/ui/pages/modplatform/ModPage.h | 6 +++--- .../pages/modplatform/flame/FlameModPage.cpp | 2 +- .../ui/pages/modplatform/flame/FlameModPage.h | 2 +- .../modplatform/modrinth/ModrinthPage.cpp | 2 +- .../pages/modplatform/modrinth/ModrinthPage.h | 2 +- 8 files changed, 30 insertions(+), 23 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 481f1c56..c71acd35 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -97,6 +97,26 @@ void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallbac } } +void ListModel::populateVersions(ModPlatform::IndexedPack const& current) +{ + auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(m_parent->debugName()).arg(current.name), APPLICATION->network()); + auto response = new QByteArray(); + QString addonId = current.addonId.toString(); + + netJob->addNetAction(Net::Download::makeByteArray(m_parent->apiProvider()->getVersionsURL(addonId), response)); + + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId]{ + m_parent->onGetVersionsSucceeded(m_parent, response, addonId); + }); + + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { + netJob->deleteLater(); + delete response; + }); + + netJob->start(); +} + void ListModel::performPaginatedSearch() { QString mcVersion = ((MinecraftInstance*)((ModPage*)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 23e544f1..2b8ff65e 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -30,6 +30,8 @@ class ListModel : public QAbstractListModel { void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); void searchWithTerm(const QString& term, const int sort); + virtual void populateVersions(const ModPlatform::IndexedPack& current); + protected slots: virtual void searchRequestFinished() = 0; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index f427d32a..68c62fb8 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -98,22 +98,7 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) ui->modSelectionButton->setText(tr("Loading versions...")); ui->modSelectionButton->setEnabled(false); - auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(debugName()).arg(current.name), APPLICATION->network()); - auto response = new QByteArray(); - QString addonId = current.addonId.toString(); - - netJob->addNetAction(Net::Download::makeByteArray(apiProvider()->getVersionsURL(addonId), response)); - - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId]{ - onModVersionSucceed(this, response, addonId); - }); - - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { - netJob->deleteLater(); - delete response; - }); - - netJob->start(); + listModel->populateVersions(current); } else { for (int i = 0; i < current.versions.size(); i++) { ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i)); diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index cd034a3a..b1681b8c 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -1,11 +1,10 @@ #pragma once -#include #include +#include "Application.h" #include "modplatform/ModAPI.h" #include "modplatform/ModIndex.h" -#include "tasks/Task.h" #include "ui/pages/BasePage.h" #include "ui/pages/modplatform/ModModel.h" @@ -37,13 +36,14 @@ class ModPage : public QWidget, public BasePage { virtual bool shouldDisplay() const override = 0; const ModAPI* apiProvider() const { return api.get(); }; + virtual void onGetVersionsSucceeded(ModPage*, QByteArray*, QString) = 0; + void openedImpl() override; bool eventFilter(QObject* watched, QEvent* event) override; BaseInstance* m_instance; protected: - virtual void onModVersionSucceed(ModPage*, QByteArray*, QString) = 0; void updateSelectionButton(); diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 47a79bd6..14766851 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -36,7 +36,7 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) bool FlameModPage::shouldDisplay() const { return true; } -void FlameModPage::onModVersionSucceed(ModPage* instance, QByteArray* response, QString addonId) +void FlameModPage::onGetVersionsSucceeded(ModPage* instance, QByteArray* response, QString addonId) { if (addonId != current.addonId) { return; // wrong request diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 89311e7f..7e8d6707 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -22,5 +22,5 @@ class FlameModPage : public ModPage { bool shouldDisplay() const override; private: - void onModVersionSucceed(ModPage*, QByteArray*, QString) override; + void onGetVersionsSucceeded(ModPage*, QByteArray*, QString) override; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 4e4a9db4..7ac9b406 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -35,7 +35,7 @@ ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) bool ModrinthPage::shouldDisplay() const { return true; } -void ModrinthPage::onModVersionSucceed(ModPage* instance, QByteArray* response, QString addonId) +void ModrinthPage::onGetVersionsSucceeded(ModPage* instance, QByteArray* response, QString addonId) { if (addonId != current.addonId) { return; } QJsonParseError parse_error; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index d92274dd..0112c5ea 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -22,5 +22,5 @@ class ModrinthPage : public ModPage { bool shouldDisplay() const override; private: - void onModVersionSucceed(ModPage*, QByteArray*, QString) override; + void onGetVersionsSucceeded(ModPage*, QByteArray*, QString) override; }; From d755174bee1b1c5ba507d1d407236190501c7940 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 6 Mar 2022 16:04:24 -0300 Subject: [PATCH 063/605] clarify some method names and comments --- launcher/ui/pages/modplatform/ModModel.cpp | 4 ++-- launcher/ui/pages/modplatform/ModModel.h | 16 +++++++++------- launcher/ui/pages/modplatform/ModPage.cpp | 2 +- launcher/ui/pages/modplatform/ModPage.h | 7 ++++--- .../ui/pages/modplatform/flame/FlameModPage.cpp | 2 +- .../ui/pages/modplatform/flame/FlameModPage.h | 2 +- .../pages/modplatform/modrinth/ModrinthPage.cpp | 2 +- .../ui/pages/modplatform/modrinth/ModrinthPage.h | 2 +- 8 files changed, 20 insertions(+), 17 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index c71acd35..4462c20b 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -97,7 +97,7 @@ void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallbac } } -void ListModel::populateVersions(ModPlatform::IndexedPack const& current) +void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) { auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(m_parent->debugName()).arg(current.name), APPLICATION->network()); auto response = new QByteArray(); @@ -106,7 +106,7 @@ void ListModel::populateVersions(ModPlatform::IndexedPack const& current) netJob->addNetAction(Net::Download::makeByteArray(m_parent->apiProvider()->getVersionsURL(addonId), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId]{ - m_parent->onGetVersionsSucceeded(m_parent, response, addonId); + m_parent->onRequestVersionsSucceeded(m_parent, response, addonId); }); QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 2b8ff65e..64c17b4a 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -22,25 +22,26 @@ class ListModel : public QAbstractListModel { int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; Qt::ItemFlags flags(const QModelIndex& index) const override; + bool canFetchMore(const QModelIndex& parent) const override; void fetchMore(const QModelIndex& parent) override; void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); void searchWithTerm(const QString& term, const int sort); - virtual void populateVersions(const ModPlatform::IndexedPack& current); + virtual void requestModVersions(const ModPlatform::IndexedPack& current); protected slots: virtual void searchRequestFinished() = 0; - - void performPaginatedSearch(); + void searchRequestFailed(QString reason); void logoFailed(QString logo); void logoLoaded(QString logo, QIcon out); - void searchRequestFailed(QString reason); + void performPaginatedSearch(); protected: virtual const char** getSorts() const = 0; @@ -51,17 +52,18 @@ class ListModel : public QAbstractListModel { ModPage* m_parent; QList modpacks; - QStringList m_failedLogos; - QStringList m_loadingLogos; + LogoMap m_logoMap; QMap waitingCallbacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; QString currentSearchTerm; int currentSort = 0; int nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; + NetJob::Ptr jobPtr; QByteArray response; - }; } // namespace ModPlatform diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 68c62fb8..a57f2903 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -98,7 +98,7 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) ui->modSelectionButton->setText(tr("Loading versions...")); ui->modSelectionButton->setEnabled(false); - listModel->populateVersions(current); + listModel->requestModVersions(current); } else { for (int i = 0; i < current.versions.size(); i++) { ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i)); diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index b1681b8c..e0d7c95c 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -23,20 +23,21 @@ class ModPage : public QWidget, public BasePage { explicit ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api); virtual ~ModPage(); - /* The name visible to the user */ + /* Affects what the user sees */ virtual QString displayName() const override = 0; virtual QIcon icon() const override = 0; virtual QString id() const override = 0; virtual QString helpPage() const override = 0; + /* Used internally */ virtual QString metaEntryBase() const = 0; - /* This only appears in the debug console */ virtual QString debugName() const = 0; + virtual bool shouldDisplay() const override = 0; const ModAPI* apiProvider() const { return api.get(); }; - virtual void onGetVersionsSucceeded(ModPage*, QByteArray*, QString) = 0; + virtual void onRequestVersionsSucceeded(ModPage*, QByteArray*, QString) = 0; void openedImpl() override; bool eventFilter(QObject* watched, QEvent* event) override; diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 14766851..6564265c 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -36,7 +36,7 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) bool FlameModPage::shouldDisplay() const { return true; } -void FlameModPage::onGetVersionsSucceeded(ModPage* instance, QByteArray* response, QString addonId) +void FlameModPage::onRequestVersionsSucceeded(ModPage* instance, QByteArray* response, QString addonId) { if (addonId != current.addonId) { return; // wrong request diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 7e8d6707..9c6f0e6c 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -22,5 +22,5 @@ class FlameModPage : public ModPage { bool shouldDisplay() const override; private: - void onGetVersionsSucceeded(ModPage*, QByteArray*, QString) override; + void onRequestVersionsSucceeded(ModPage*, QByteArray*, QString) override; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 7ac9b406..944f8afb 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -35,7 +35,7 @@ ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) bool ModrinthPage::shouldDisplay() const { return true; } -void ModrinthPage::onGetVersionsSucceeded(ModPage* instance, QByteArray* response, QString addonId) +void ModrinthPage::onRequestVersionsSucceeded(ModPage* instance, QByteArray* response, QString addonId) { if (addonId != current.addonId) { return; } QJsonParseError parse_error; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 0112c5ea..7b1d0a00 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -22,5 +22,5 @@ class ModrinthPage : public ModPage { bool shouldDisplay() const override; private: - void onGetVersionsSucceeded(ModPage*, QByteArray*, QString) override; + void onRequestVersionsSucceeded(ModPage*, QByteArray*, QString) override; }; From 39bd04f06ff42623f7349096d707c4a877fc7cd7 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 6 Mar 2022 16:45:39 -0300 Subject: [PATCH 064/605] refactor: use Enum instead of raw int for ModLoaderType --- launcher/modplatform/ModAPI.h | 11 +++++++- launcher/modplatform/flame/FlameAPI.h | 5 ++-- launcher/modplatform/modrinth/ModrinthAPI.h | 29 +++++++++++++++++++-- launcher/ui/pages/modplatform/ModModel.cpp | 3 ++- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index e60fa8e0..4d22a63d 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -6,7 +6,16 @@ class ModAPI { public: virtual ~ModAPI() = default; - inline virtual QString getModSearchURL(int, QString, QString, bool, QString) const { return ""; }; + // https://docs.curseforge.com/?http#tocS_ModLoaderType + enum ModLoaderType { + Any = 0, + Forge = 1, + Cauldron = 2, + LiteLoader = 3, + Fabric = 4 + }; + + inline virtual QString getModSearchURL(int, QString, QString, ModLoaderType, QString) const { return ""; }; inline virtual QString getVersionsURL(const QString& addonId) const { return ""; }; inline virtual QString getAuthorURL(const QString& name) const { return ""; }; }; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 6e2b9e25..b49aeb24 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -4,7 +4,8 @@ class FlameAPI : public ModAPI { public: - inline QString getModSearchURL(int index, QString searchFilter, QString sort, bool fabricCompatible, QString version) const override + + inline QString getModSearchURL(int index, QString searchFilter, QString sort, ModLoaderType modLoader, QString version) const override { return QString("https://addons-ecs.forgesvc.net/api/v2/addon/search?" "gameId=432&" "categoryId=0&" "sectionId=6&" @@ -14,7 +15,7 @@ class FlameAPI : public ModAPI { .arg(index) .arg(searchFilter) .arg(sort) - .arg(fabricCompatible ? 4 : 1) // Enum: https://docs.curseforge.com/?http#tocS_ModLoaderType + .arg(modLoader) .arg(version); }; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 4ae8b8f9..44a362c8 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -2,17 +2,24 @@ #include "modplatform/ModAPI.h" +#include + class ModrinthAPI : public ModAPI { public: - inline QString getModSearchURL(int offset, QString query, QString sort, bool fabricCompatible, QString version) const override + inline QString getModSearchURL(int offset, QString query, QString sort, ModLoaderType modLoader, QString version) const override { + if(!validateModLoader(modLoader)){ + qWarning() << "Modrinth only have Forge and Fabric-compatible mods!"; + return ""; + } + return QString("https://api.modrinth.com/v2/search?" "offset=%1&" "limit=25&" "query=%2&" "index=%3&" "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]") .arg(offset) .arg(query) .arg(sort) - .arg(fabricCompatible ? "fabric" : "forge") + .arg(getModLoaderString(modLoader)) .arg(version); }; @@ -22,4 +29,22 @@ class ModrinthAPI : public ModAPI { }; inline QString getAuthorURL(const QString& name) const override { return "https://modrinth.com/user/" + name; }; + + private: + inline bool validateModLoader(ModLoaderType modLoader) const{ + return modLoader == Any || modLoader == Forge || modLoader == Fabric; + } + + inline QString getModLoaderString(ModLoaderType modLoader) const{ + switch(modLoader){ + case Any: + return "fabric, forge"; + case Forge: + return "forge"; + case Fabric: + return "fabric"; + default: + return ""; + } + } }; diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 4462c20b..5be578c1 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -125,7 +125,8 @@ void ListModel::performPaginatedSearch() ->getComponentVersion("net.fabricmc.fabric-loader") .isEmpty(); auto netJob = new NetJob(QString("%1::Search").arg(m_parent->debugName()), APPLICATION->network()); - auto searchUrl = m_parent->apiProvider()->getModSearchURL(nextSearchOffset, currentSearchTerm, getSorts()[currentSort], hasFabric, mcVersion); + auto searchUrl = m_parent->apiProvider()->getModSearchURL( + nextSearchOffset, currentSearchTerm, getSorts()[currentSort], hasFabric ? ModAPI::Fabric : ModAPI::Forge, mcVersion); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; From f714adf6d2cc94f20ba37f2776d0d61e22267f0e Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 7 Mar 2022 16:22:57 -0300 Subject: [PATCH 065/605] refactor: move NetJob away from ModModel to ModAPI This is done so that 1. ModAPI behaves more like an actual API instead of just a helper, and 2. Allows for more easily creating other mod providers that may or may not use network tasks (foreshadowing lol) --- launcher/modplatform/ModAPI.h | 35 ++++-- launcher/modplatform/flame/FlameAPI.h | 91 ++++++++++++-- launcher/modplatform/modrinth/ModrinthAPI.h | 117 +++++++++++++----- launcher/ui/pages/modplatform/ModModel.cpp | 47 +++---- launcher/ui/pages/modplatform/ModModel.h | 13 +- launcher/ui/pages/modplatform/ModPage.h | 2 +- .../pages/modplatform/flame/FlameModModel.cpp | 10 +- .../pages/modplatform/flame/FlameModModel.h | 2 +- .../pages/modplatform/flame/FlameModPage.cpp | 12 +- .../ui/pages/modplatform/flame/FlameModPage.h | 2 +- .../modplatform/modrinth/ModrinthModel.cpp | 13 +- .../modplatform/modrinth/ModrinthModel.h | 4 +- .../modplatform/modrinth/ModrinthPage.cpp | 15 +-- .../pages/modplatform/modrinth/ModrinthPage.h | 2 +- 14 files changed, 230 insertions(+), 135 deletions(-) diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index 4d22a63d..8503d7fc 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -1,21 +1,30 @@ #pragma once +#include #include +namespace ModPlatform { +class ListModel; +} + class ModAPI { - public: - virtual ~ModAPI() = default; + protected: + using CallerType = ModPlatform::ListModel; - // https://docs.curseforge.com/?http#tocS_ModLoaderType - enum ModLoaderType { - Any = 0, - Forge = 1, - Cauldron = 2, - LiteLoader = 3, - Fabric = 4 - }; + public: + virtual ~ModAPI() = default; - inline virtual QString getModSearchURL(int, QString, QString, ModLoaderType, QString) const { return ""; }; - inline virtual QString getVersionsURL(const QString& addonId) const { return ""; }; - inline virtual QString getAuthorURL(const QString& name) const { return ""; }; + // https://docs.curseforge.com/?http#tocS_ModLoaderType + enum ModLoaderType { Any = 0, Forge = 1, Cauldron = 2, LiteLoader = 3, Fabric = 4 }; + + struct SearchArgs { + int offset; + QString search; + QString sorting; + ModLoaderType mod_loader; + QString version; + }; + + inline virtual void searchMods(CallerType* caller, SearchArgs&& args) const {}; + inline virtual void getVersions(CallerType* caller, const QString& addonId, const QString& debugName = "") const {}; }; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index b49aeb24..be88df65 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -1,28 +1,93 @@ #pragma once #include "modplatform/ModAPI.h" +#include "ui/pages/modplatform/ModModel.h" + +#include "Application.h" +#include "net/NetJob.h" class FlameAPI : public ModAPI { public: - - inline QString getModSearchURL(int index, QString searchFilter, QString sort, ModLoaderType modLoader, QString version) const override + inline void searchMods(CallerType* caller, SearchArgs&& args) const override { - return QString("https://addons-ecs.forgesvc.net/api/v2/addon/search?" - "gameId=432&" "categoryId=0&" "sectionId=6&" + auto netJob = new NetJob(QString("Flame::Search"), APPLICATION->network()); + auto searchUrl = getModSearchURL(args); - "index=%1&" "pageSize=25&" "searchFilter=%2&" - "sort=%3&" "modLoaderType=%4&" "gameVersion=%5") - .arg(index) - .arg(searchFilter) - .arg(sort) - .arg(modLoader) - .arg(version); + auto response = new QByteArray(); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + + QObject::connect(netJob, &NetJob::started, caller, [caller, netJob]{ caller->setActiveJob(netJob); }); + QObject::connect(netJob, &NetJob::failed, caller, &CallerType::searchRequestFailed); + QObject::connect(netJob, &NetJob::succeeded, caller, [caller, response] { + 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; + } + + caller->searchRequestFinished(doc); + }); + + netJob->start(); }; - inline QString getVersionsURL(const QString& addonId) const override + inline void getVersions(CallerType* caller, const QString& addonId, const QString& debugName = "Flame") const override + { + auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(debugName).arg(addonId), APPLICATION->network()); + auto response = new QByteArray(); + + netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(addonId), response)); + + QObject::connect(netJob, &NetJob::succeeded, caller, [response, debugName, caller, addonId] { + 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; + return; + } + + caller->versionRequestSucceeded(doc, addonId); + }); + + QObject::connect(netJob, &NetJob::finished, caller, [response, netJob] { + netJob->deleteLater(); + delete response; + }); + + netJob->start(); + }; + + private: + inline QString getModSearchURL(SearchArgs& args) const + { + return QString( + "https://addons-ecs.forgesvc.net/api/v2/addon/search?" + "gameId=432&" + "categoryId=0&" + "sectionId=6&" + + "index=%1&" + "pageSize=25&" + "searchFilter=%2&" + "sort=%3&" + "modLoaderType=%4&" + "gameVersion=%5") + .arg(args.offset) + .arg(args.search) + .arg(args.sorting) + .arg(args.mod_loader) + .arg(args.version); + }; + + inline QString getVersionsURL(const QString& addonId) const { return QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId); }; - inline QString getAuthorURL(const QString& name) const override { return ""; }; + inline QString getAuthorURL(const QString& name) const { return ""; }; }; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 44a362c8..84cc561b 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -1,50 +1,111 @@ #pragma once #include "modplatform/ModAPI.h" +#include "ui/pages/modplatform/ModModel.h" + +#include "Application.h" +#include "net/NetJob.h" #include class ModrinthAPI : public ModAPI { public: - inline QString getModSearchURL(int offset, QString query, QString sort, ModLoaderType modLoader, QString version) const override - { - if(!validateModLoader(modLoader)){ + inline void searchMods(CallerType* caller, SearchArgs&& args) const override + { + auto netJob = new NetJob(QString("Modrinth::Search"), APPLICATION->network()); + auto searchUrl = getModSearchURL(args); + + auto response = new QByteArray(); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + + QObject::connect(netJob, &NetJob::started, caller, [caller, netJob]{ caller->setActiveJob(netJob); }); + QObject::connect(netJob, &NetJob::failed, caller, &CallerType::searchRequestFailed); + QObject::connect(netJob, &NetJob::succeeded, caller, [caller, response] { + 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; + } + + caller->searchRequestFinished(doc); + }); + + netJob->start(); + }; + + inline void getVersions(CallerType* caller, const QString& addonId, const QString& debugName = "Modrinth") const override + { + auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(debugName).arg(addonId), APPLICATION->network()); + auto response = new QByteArray(); + + netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(addonId), response)); + + QObject::connect(netJob, &NetJob::succeeded, caller, [response, debugName, caller, addonId] { + 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; + return; + } + + caller->versionRequestSucceeded(doc, addonId); + }); + + QObject::connect(netJob, &NetJob::finished, caller, [response, netJob] { + netJob->deleteLater(); + delete response; + }); + + netJob->start(); + }; + + inline QString getAuthorURL(const QString& name) const { return "https://modrinth.com/user/" + name; }; + + private: + inline QString getModSearchURL(SearchArgs& args) const + { + if (!validateModLoader(args.mod_loader)) { qWarning() << "Modrinth only have Forge and Fabric-compatible mods!"; return ""; } - return QString("https://api.modrinth.com/v2/search?" - "offset=%1&" "limit=25&" "query=%2&" "index=%3&" - "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]") - .arg(offset) - .arg(query) - .arg(sort) - .arg(getModLoaderString(modLoader)) - .arg(version); + return QString( + "https://api.modrinth.com/v2/search?" + "offset=%1&" + "limit=25&" + "query=%2&" + "index=%3&" + "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]") + .arg(args.offset) + .arg(args.search) + .arg(args.sorting) + .arg(getModLoaderString(args.mod_loader)) + .arg(args.version); }; - inline QString getVersionsURL(const QString& addonId) const override + inline QString getVersionsURL(const QString& addonId) const { return QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId); }; - inline QString getAuthorURL(const QString& name) const override { return "https://modrinth.com/user/" + name; }; + inline bool validateModLoader(ModLoaderType modLoader) const { return modLoader == Any || modLoader == Forge || modLoader == Fabric; } - private: - inline bool validateModLoader(ModLoaderType modLoader) const{ - return modLoader == Any || modLoader == Forge || modLoader == Fabric; - } - - inline QString getModLoaderString(ModLoaderType modLoader) const{ - switch(modLoader){ - case Any: - return "fabric, forge"; - case Forge: - return "forge"; - case Fabric: - return "fabric"; - default: - return ""; + inline QString getModLoaderString(ModLoaderType modLoader) const + { + switch (modLoader) { + case Any: + return "fabric, forge"; + case Forge: + return "forge"; + case Fabric: + return "fabric"; + default: + return ""; } } }; diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 5be578c1..705f384e 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -91,30 +91,16 @@ void ListModel::fetchMore(const QModelIndex& parent) void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) { if (m_logoMap.contains(logo)) { - callback(APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + callback(APPLICATION->metacache() + ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))) + ->getFullPath()); } else { requestLogo(logo, logoUrl); } } -void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) -{ - auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(m_parent->debugName()).arg(current.name), APPLICATION->network()); - auto response = new QByteArray(); - QString addonId = current.addonId.toString(); - - netJob->addNetAction(Net::Download::makeByteArray(m_parent->apiProvider()->getVersionsURL(addonId), response)); - - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId]{ - m_parent->onRequestVersionsSucceeded(m_parent, response, addonId); - }); - - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { - netJob->deleteLater(); - delete response; - }); - - netJob->start(); +void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) { + m_parent->apiProvider()->getVersions(this, current.addonId.toString(), m_parent->debugName()); } void ListModel::performPaginatedSearch() @@ -124,16 +110,9 @@ void ListModel::performPaginatedSearch() ->getPackProfile() ->getComponentVersion("net.fabricmc.fabric-loader") .isEmpty(); - auto netJob = new NetJob(QString("%1::Search").arg(m_parent->debugName()), APPLICATION->network()); - auto searchUrl = m_parent->apiProvider()->getModSearchURL( - nextSearchOffset, currentSearchTerm, getSorts()[currentSort], hasFabric ? ModAPI::Fabric : ModAPI::Forge, mcVersion); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); - jobPtr = netJob; - jobPtr->start(); - - QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); - QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); + m_parent->apiProvider()->searchMods( + this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], hasFabric ? ModAPI::Fabric : ModAPI::Forge, mcVersion }); } void ListModel::searchWithTerm(const QString& term, const int sort) @@ -160,9 +139,7 @@ void ListModel::searchRequestFailed(QString reason) if (jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { // 409 Gone, notify user to update QMessageBox::critical(nullptr, tr("Error"), - QString("%1 %2") - .arg(m_parent->displayName()) - .arg(tr("API version too old!\nPlease update PolyMC!"))); + QString("%1 %2").arg(m_parent->displayName()).arg(tr("API version too old!\nPlease update PolyMC!"))); // self-destruct ((ModDownloadDialog*)((ModPage*)parent())->parentWidget())->reject(); } @@ -180,11 +157,17 @@ void ListModel::searchRequestFailed(QString reason) } } +void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) +{ + m_parent->onRequestVersionsSucceeded(doc, addonId); +} + void ListModel::requestLogo(QString logo, QString url) { if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; } - MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); + MetaEntryPtr entry = + APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 64c17b4a..28bf05bb 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -1,9 +1,10 @@ #pragma once +#include #include -#include "modplatform/ModIndex.h" #include "modplatform/ModAPI.h" +#include "modplatform/ModIndex.h" #include "net/NetJob.h" class ModPage; @@ -26,6 +27,8 @@ class ListModel : public QAbstractListModel { QVariant data(const QModelIndex& index, int role) const override; Qt::ItemFlags flags(const QModelIndex& index) const override; + void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } + bool canFetchMore(const QModelIndex& parent) const override; void fetchMore(const QModelIndex& parent) override; @@ -34,10 +37,14 @@ class ListModel : public QAbstractListModel { virtual void requestModVersions(const ModPlatform::IndexedPack& current); - protected slots: - virtual void searchRequestFinished() = 0; + public slots: + virtual void searchRequestFinished(QJsonDocument& doc) = 0; void searchRequestFailed(QString reason); + void versionRequestSucceeded(QJsonDocument doc, QString addonId); + + protected slots: + void logoFailed(QString logo); void logoLoaded(QString logo, QIcon out); diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index e0d7c95c..aa0e8894 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -37,7 +37,7 @@ class ModPage : public QWidget, public BasePage { virtual bool shouldDisplay() const override = 0; const ModAPI* apiProvider() const { return api.get(); }; - virtual void onRequestVersionsSucceeded(ModPage*, QByteArray*, QString) = 0; + virtual void onRequestVersionsSucceeded(QJsonDocument&, QString) = 0; void openedImpl() override; bool eventFilter(QObject* watched, QEvent* event) override; diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index 283f9ce7..cff29a79 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -11,18 +11,10 @@ ListModel::ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {} ListModel::~ListModel() {} -void FlameMod::ListModel::searchRequestFinished() +void FlameMod::ListModel::searchRequestFinished(QJsonDocument& doc) { jobPtr.reset(); - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; - return; - } - QList newList; auto packs = doc.array(); for(auto packRaw : packs) { diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h index ae919e63..022ec32e 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h @@ -30,7 +30,7 @@ class ListModel : public ModPlatform::ListModel { virtual ~ListModel(); private slots: - void searchRequestFinished() override; + void searchRequestFinished(QJsonDocument& doc) override; private: const char** getSorts() const override; diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 6564265c..19f58280 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -36,23 +36,17 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) bool FlameModPage::shouldDisplay() const { return true; } -void FlameModPage::onRequestVersionsSucceeded(ModPage* instance, QByteArray* response, QString addonId) +void FlameModPage::onRequestVersionsSucceeded(QJsonDocument& doc, QString addonId) { if (addonId != 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 Flame at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } + QJsonArray arr = doc.array(); try { FlameMod::loadIndexedPackVersions(current, arr, APPLICATION->network(), m_instance); } catch (const JSONValidationError& e) { - qDebug() << *response; + qDebug() << doc; qWarning() << "Error while reading Flame mod version: " << e.cause(); } auto packProfile = ((MinecraftInstance*)m_instance)->getPackProfile(); diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 9c6f0e6c..f15b51ec 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -22,5 +22,5 @@ class FlameModPage : public ModPage { bool shouldDisplay() const override; private: - void onRequestVersionsSucceeded(ModPage*, QByteArray*, QString) override; + void onRequestVersionsSucceeded(QJsonDocument&, QString) override; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index dc3d1469..784b1128 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -10,19 +10,10 @@ ListModel::ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent) {} ListModel::~ListModel() {} -void Modrinth::ListModel::searchRequestFinished() +void Modrinth::ListModel::searchRequestFinished(QJsonDocument& doc) { jobPtr.reset(); - - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << response; - return; - } - + QList newList; auto packs = doc.object().value("hits").toArray(); for (auto packRaw : packs) { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index b8937b50..d095b18c 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -29,8 +29,8 @@ class ListModel : public ModPlatform::ListModel { ListModel(ModrinthPage* parent); virtual ~ListModel(); - private slots: - void searchRequestFinished() override; + public slots: + void searchRequestFinished(QJsonDocument& doc) override; private: const char** getSorts() const override; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 944f8afb..fa703e38 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -35,22 +35,15 @@ ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) bool ModrinthPage::shouldDisplay() const { return true; } -void ModrinthPage::onRequestVersionsSucceeded(ModPage* instance, QByteArray* response, QString addonId) +void ModrinthPage::onRequestVersionsSucceeded(QJsonDocument& response, QString addonId) { if (addonId != current.addonId) { return; } - 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; - } - QJsonArray arr = doc.array(); + + QJsonArray arr = response.array(); try { Modrinth::loadIndexedPackVersions(current, arr, APPLICATION->network(), m_instance); } catch (const JSONValidationError& e) { - qDebug() << *response; + qDebug() << response; qWarning() << "Error while reading Modrinth mod version: " << e.cause(); } auto packProfile = ((MinecraftInstance*)m_instance)->getPackProfile(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 7b1d0a00..f6d1eef0 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -22,5 +22,5 @@ class ModrinthPage : public ModPage { bool shouldDisplay() const override; private: - void onRequestVersionsSucceeded(ModPage*, QByteArray*, QString) override; + void onRequestVersionsSucceeded(QJsonDocument&, QString) override; }; From 16bfafa29e2cb54e1553c813cab0fff5203f8c60 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 7 Mar 2022 16:46:08 -0300 Subject: [PATCH 066/605] refactor: de-duplicate common code in network mod APIs --- launcher/CMakeLists.txt | 11 +++ launcher/modplatform/ModAPI.h | 5 +- launcher/modplatform/flame/FlameAPI.h | 65 +--------------- .../modplatform/helpers/NetworkModAPI.cpp | 60 +++++++++++++++ launcher/modplatform/helpers/NetworkModAPI.h | 13 ++++ launcher/modplatform/modrinth/ModrinthAPI.h | 74 +++---------------- launcher/ui/pages/modplatform/ModModel.cpp | 10 ++- launcher/ui/pages/modplatform/ModModel.h | 4 +- 8 files changed, 109 insertions(+), 133 deletions(-) create mode 100644 launcher/modplatform/helpers/NetworkModAPI.cpp create mode 100644 launcher/modplatform/helpers/NetworkModAPI.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 0dcda925..48370c96 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -492,6 +492,16 @@ set(META_SOURCES meta/Index.h ) +set(API_SOURCES + modplatform/ModAPI.h + + modplatform/flame/FlameAPI.h + modplatform/modrinth/ModrinthAPI.h + + modplatform/helpers/NetworkModAPI.h + modplatform/helpers/NetworkModAPI.cpp +) + set(FTB_SOURCES modplatform/legacy_ftb/PackFetchTask.h modplatform/legacy_ftb/PackFetchTask.cpp @@ -572,6 +582,7 @@ set(LOGIC_SOURCES ${TOOLS_SOURCES} ${META_SOURCES} ${ICONS_SOURCES} + ${API_SOURCES} ${FTB_SOURCES} ${FLAME_SOURCES} ${MODRINTH_SOURCES} diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index 8503d7fc..5c7c6349 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -1,6 +1,5 @@ #pragma once -#include #include namespace ModPlatform { @@ -25,6 +24,6 @@ class ModAPI { QString version; }; - inline virtual void searchMods(CallerType* caller, SearchArgs&& args) const {}; - inline virtual void getVersions(CallerType* caller, const QString& addonId, const QString& debugName = "") const {}; + virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0; + virtual void getVersions(CallerType* caller, const QString& addonId) const = 0; }; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index be88df65..f7f993d7 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -1,67 +1,8 @@ #pragma once -#include "modplatform/ModAPI.h" -#include "ui/pages/modplatform/ModModel.h" - -#include "Application.h" -#include "net/NetJob.h" - -class FlameAPI : public ModAPI { - public: - inline void searchMods(CallerType* caller, SearchArgs&& args) const override - { - auto netJob = new NetJob(QString("Flame::Search"), APPLICATION->network()); - auto searchUrl = getModSearchURL(args); - - auto response = new QByteArray(); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); - - QObject::connect(netJob, &NetJob::started, caller, [caller, netJob]{ caller->setActiveJob(netJob); }); - QObject::connect(netJob, &NetJob::failed, caller, &CallerType::searchRequestFailed); - QObject::connect(netJob, &NetJob::succeeded, caller, [caller, response] { - 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; - } - - caller->searchRequestFinished(doc); - }); - - netJob->start(); - }; - - inline void getVersions(CallerType* caller, const QString& addonId, const QString& debugName = "Flame") const override - { - auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(debugName).arg(addonId), APPLICATION->network()); - auto response = new QByteArray(); - - netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(addonId), response)); - - QObject::connect(netJob, &NetJob::succeeded, caller, [response, debugName, caller, addonId] { - 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; - return; - } - - caller->versionRequestSucceeded(doc, addonId); - }); - - QObject::connect(netJob, &NetJob::finished, caller, [response, netJob] { - netJob->deleteLater(); - delete response; - }); - - netJob->start(); - }; +#include "modplatform/helpers/NetworkModAPI.h" +class FlameAPI : public NetworkModAPI { private: inline QString getModSearchURL(SearchArgs& args) const { @@ -88,6 +29,4 @@ class FlameAPI : public ModAPI { { return QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId); }; - - inline QString getAuthorURL(const QString& name) const { return ""; }; }; diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp new file mode 100644 index 00000000..4b066102 --- /dev/null +++ b/launcher/modplatform/helpers/NetworkModAPI.cpp @@ -0,0 +1,60 @@ +#include "NetworkModAPI.h" + +#include "ui/pages/modplatform/ModModel.h" + +#include "Application.h" +#include "net/NetJob.h" + +void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const +{ + auto netJob = new NetJob(QString("Modrinth::Search"), APPLICATION->network()); + auto searchUrl = getModSearchURL(args); + + auto response = new QByteArray(); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + + QObject::connect(netJob, &NetJob::started, caller, [caller, netJob] { caller->setActiveJob(netJob); }); + QObject::connect(netJob, &NetJob::failed, caller, &CallerType::searchRequestFailed); + QObject::connect(netJob, &NetJob::succeeded, caller, [caller, response] { + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + caller->searchRequestFinished(doc); + }); + + netJob->start(); +}; + +void NetworkModAPI::getVersions(CallerType* caller, const QString& addonId) const +{ + auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(caller->debugName()).arg(addonId), APPLICATION->network()); + auto response = new QByteArray(); + + netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(addonId), response)); + + QObject::connect(netJob, &NetJob::succeeded, caller, [response, caller, addonId] { + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + caller->versionRequestSucceeded(doc, addonId); + }); + + QObject::connect(netJob, &NetJob::finished, caller, [response, netJob] { + netJob->deleteLater(); + delete response; + }); + + netJob->start(); +}; diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h new file mode 100644 index 00000000..65192046 --- /dev/null +++ b/launcher/modplatform/helpers/NetworkModAPI.h @@ -0,0 +1,13 @@ +#pragma once + +#include "modplatform/ModAPI.h" + +class NetworkModAPI : public ModAPI { + public: + void searchMods(CallerType* caller, SearchArgs&& args) const override; + void getVersions(CallerType* caller, const QString& addonId) const override; + + protected: + virtual QString getModSearchURL(SearchArgs& args) const = 0; + virtual QString getVersionsURL(const QString& addonId) const = 0; +}; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 84cc561b..1cc51ff2 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -1,73 +1,15 @@ #pragma once -#include "modplatform/ModAPI.h" -#include "ui/pages/modplatform/ModModel.h" - -#include "Application.h" -#include "net/NetJob.h" +#include "modplatform/helpers/NetworkModAPI.h" #include -class ModrinthAPI : public ModAPI { +class ModrinthAPI : public NetworkModAPI { public: - inline void searchMods(CallerType* caller, SearchArgs&& args) const override - { - auto netJob = new NetJob(QString("Modrinth::Search"), APPLICATION->network()); - auto searchUrl = getModSearchURL(args); - - auto response = new QByteArray(); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); - - QObject::connect(netJob, &NetJob::started, caller, [caller, netJob]{ caller->setActiveJob(netJob); }); - QObject::connect(netJob, &NetJob::failed, caller, &CallerType::searchRequestFailed); - QObject::connect(netJob, &NetJob::succeeded, caller, [caller, response] { - 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; - } - - caller->searchRequestFinished(doc); - }); - - netJob->start(); - }; - - inline void getVersions(CallerType* caller, const QString& addonId, const QString& debugName = "Modrinth") const override - { - auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(debugName).arg(addonId), APPLICATION->network()); - auto response = new QByteArray(); - - netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(addonId), response)); - - QObject::connect(netJob, &NetJob::succeeded, caller, [response, debugName, caller, addonId] { - 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; - return; - } - - caller->versionRequestSucceeded(doc, addonId); - }); - - QObject::connect(netJob, &NetJob::finished, caller, [response, netJob] { - netJob->deleteLater(); - delete response; - }); - - netJob->start(); - }; - inline QString getAuthorURL(const QString& name) const { return "https://modrinth.com/user/" + name; }; private: - inline QString getModSearchURL(SearchArgs& args) const + inline QString getModSearchURL(SearchArgs& args) const override { if (!validateModLoader(args.mod_loader)) { qWarning() << "Modrinth only have Forge and Fabric-compatible mods!"; @@ -88,13 +30,11 @@ class ModrinthAPI : public ModAPI { .arg(args.version); }; - inline QString getVersionsURL(const QString& addonId) const + inline QString getVersionsURL(const QString& addonId) const override { return QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId); }; - inline bool validateModLoader(ModLoaderType modLoader) const { return modLoader == Any || modLoader == Forge || modLoader == Fabric; } - inline QString getModLoaderString(ModLoaderType modLoader) const { switch (modLoader) { @@ -108,4 +48,10 @@ class ModrinthAPI : public ModAPI { return ""; } } + + inline bool validateModLoader(ModLoaderType modLoader) const + { + return modLoader == Any || modLoader == Forge || modLoader == Fabric; + } + }; diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 705f384e..015fcf7d 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -53,6 +53,11 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return QVariant(); } +QString ListModel::debugName() const +{ + return m_parent->debugName(); +} + void ListModel::logoLoaded(QString logo, QIcon out) { m_loadingLogos.removeAll(logo); @@ -99,8 +104,9 @@ void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallbac } } -void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) { - m_parent->apiProvider()->getVersions(this, current.addonId.toString(), m_parent->debugName()); +void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) +{ + m_parent->apiProvider()->getVersions(this, current.addonId.toString()); } void ListModel::performPaginatedSearch() diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 28bf05bb..e971149c 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include "modplatform/ModAPI.h" @@ -24,6 +23,9 @@ class ListModel : public QAbstractListModel { int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; + QString debugName() const; + + /* Retrieve information from the model at a given index with the given role */ QVariant data(const QModelIndex& index, int role) const override; Qt::ItemFlags flags(const QModelIndex& index) const override; From b131d3b2ecbe6a9be35088d8411927bcd30de896 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 7 Mar 2022 17:46:18 -0300 Subject: [PATCH 067/605] refactor: move more common code to base class Also removes unused imports and organize the ModModel header --- launcher/ui/pages/modplatform/ModModel.cpp | 56 +++++++++++-------- launcher/ui/pages/modplatform/ModModel.h | 22 ++++---- .../pages/modplatform/flame/FlameModModel.cpp | 48 +--------------- .../pages/modplatform/flame/FlameModModel.h | 34 ++++------- .../modplatform/modrinth/ModrinthModel.cpp | 44 +-------------- .../modplatform/modrinth/ModrinthModel.h | 31 +++------- 6 files changed, 65 insertions(+), 170 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 015fcf7d..b8b90f84 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -1,6 +1,6 @@ #include "ModModel.h" -#include "ModPage.h" +#include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" @@ -11,18 +11,6 @@ namespace ModPlatform { ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) {} -ListModel::~ListModel() {} - -int ListModel::rowCount(const QModelIndex& parent) const -{ - return modpacks.size(); -} - -int ListModel::columnCount(const QModelIndex& parent) const -{ - return 1; -} - QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); @@ -73,16 +61,6 @@ void ListModel::logoFailed(QString logo) m_loadingLogos.removeAll(logo); } -Qt::ItemFlags ListModel::flags(const QModelIndex& index) const -{ - return QAbstractListModel::flags(index); -} - -bool ListModel::canFetchMore(const QModelIndex& parent) const -{ - return searchState == CanPossiblyFetchMore; -} - void ListModel::fetchMore(const QModelIndex& parent) { if (parent.isValid()) return; @@ -140,6 +118,38 @@ void ListModel::searchWithTerm(const QString& term, const int sort) performPaginatedSearch(); } +void ListModel::searchRequestFinished(QJsonDocument& doc) +{ + jobPtr.reset(); + + QList newList; + auto packs = documentToArray(doc); + + for (auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + ModPlatform::IndexedPack pack; + try { + loadIndexedPack(pack, packObj); + newList.append(pack); + } catch (const JSONValidationError& e) { + qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause(); + continue; + } + } + + if (packs.size() < 25) { + searchState = Finished; + } else { + nextSearchOffset += 25; + searchState = CanPossiblyFetchMore; + } + + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + void ListModel::searchRequestFailed(QString reason) { if (jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index e971149c..e0cc098d 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -18,29 +18,30 @@ class ListModel : public QAbstractListModel { public: ListModel(ModPage* parent); - virtual ~ListModel(); + virtual ~ListModel() = default; - int rowCount(const QModelIndex& parent) const override; - int columnCount(const QModelIndex& parent) const override; + inline int rowCount(const QModelIndex& parent) const override { return modpacks.size(); }; + inline int columnCount(const QModelIndex& parent) const override { return 1; }; + inline Qt::ItemFlags flags(const QModelIndex& index) const override { return QAbstractListModel::flags(index); }; QString debugName() const; /* Retrieve information from the model at a given index with the given role */ QVariant data(const QModelIndex& index, int role) const override; - Qt::ItemFlags flags(const QModelIndex& index) const override; - void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } + inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } - bool canFetchMore(const QModelIndex& parent) const override; + /* Ask the API for more information */ void fetchMore(const QModelIndex& parent) override; + void searchWithTerm(const QString& term, const int sort); + void requestModVersions(const ModPlatform::IndexedPack& current); void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); - void searchWithTerm(const QString& term, const int sort); - virtual void requestModVersions(const ModPlatform::IndexedPack& current); + inline bool canFetchMore(const QModelIndex& parent) const override { return searchState == CanPossiblyFetchMore; }; public slots: - virtual void searchRequestFinished(QJsonDocument& doc) = 0; + void searchRequestFinished(QJsonDocument& doc); void searchRequestFailed(QString reason); void versionRequestSucceeded(QJsonDocument doc, QString addonId); @@ -53,6 +54,8 @@ class ListModel : public QAbstractListModel { void performPaginatedSearch(); protected: + virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; + virtual QJsonArray documentToArray(QJsonDocument& obj) const = 0; virtual const char** getSorts() const = 0; void requestLogo(QString file, QString url); @@ -73,6 +76,5 @@ class ListModel : public QAbstractListModel { enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; NetJob::Ptr jobPtr; - QByteArray response; }; } // namespace ModPlatform diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index cff29a79..7588a714 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -1,53 +1,7 @@ #include "FlameModModel.h" -#include "FlameModPage.h" -#include "minecraft/PackProfile.h" - -#include namespace FlameMod { -ListModel::ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {} - -ListModel::~ListModel() {} - - -void FlameMod::ListModel::searchRequestFinished(QJsonDocument& doc) -{ - jobPtr.reset(); - - QList newList; - auto packs = doc.array(); - for(auto packRaw : packs) { - auto packObj = packRaw.toObject(); - - ModPlatform::IndexedPack pack; - try - { - FlameMod::loadIndexedPack(pack, packObj); - newList.append(pack); - } - catch(const JSONValidationError &e) - { - qWarning() << "Error while loading mod from Flame: " << e.cause(); - continue; - } - } - if(packs.size() < 25) { - searchState = Finished; - } else { - nextSearchOffset += 25; - searchState = CanPossiblyFetchMore; - } - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); - modpacks.append(newList); - endInsertRows(); -} - -const char* sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; - -const char** FlameMod::ListModel::getSorts() const -{ - return sorts; -} +const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; } // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h index 022ec32e..204834c9 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h @@ -1,39 +1,25 @@ #pragma once -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "BaseInstance.h" #include "FlameModPage.h" #include "modplatform/flame/FlameModIndex.h" namespace FlameMod { -typedef std::function LogoCallback; - class ListModel : public ModPlatform::ListModel { Q_OBJECT public: - ListModel(FlameModPage* parent); - virtual ~ListModel(); - - private slots: - void searchRequestFinished(QJsonDocument& doc) override; + ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {} +; + virtual ~ListModel() = default; private: - const char** getSorts() const override; + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override { FlameMod::loadIndexedPack(m, obj); }; + + QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.array(); }; + + static const char* sorts[6]; + const char** getSorts() const override { return sorts; }; }; -} // namespace Modrinth +} // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 784b1128..9361546e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -1,49 +1,7 @@ #include "ModrinthModel.h" -#include "ModrinthPage.h" -#include "minecraft/MinecraftInstance.h" - -#include namespace Modrinth { -ListModel::ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent) {} - -ListModel::~ListModel() {} - -void Modrinth::ListModel::searchRequestFinished(QJsonDocument& doc) -{ - jobPtr.reset(); - - QList newList; - auto packs = doc.object().value("hits").toArray(); - for (auto packRaw : packs) { - auto packObj = packRaw.toObject(); - - ModPlatform::IndexedPack pack; - try { - Modrinth::loadIndexedPack(pack, packObj); - newList.append(pack); - } catch (const JSONValidationError& e) { - qWarning() << "Error while loading mod from Modrinth: " << e.cause(); - continue; - } - } - if (packs.size() < 25) { - searchState = Finished; - } else { - nextSearchOffset += 25; - searchState = CanPossiblyFetchMore; - } - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); - modpacks.append(newList); - endInsertRows(); -} - -const char* sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; - -const char** Modrinth::ListModel::getSorts() const -{ - return sorts; -} +const char* ListModel::sorts[5] { "relevance", "downloads", "follows", "updated", "newest" }; } // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index d095b18c..9137190d 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -1,39 +1,24 @@ #pragma once -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "BaseInstance.h" #include "ModrinthPage.h" #include "modplatform/modrinth/ModrinthPackIndex.h" namespace Modrinth { -typedef std::function LogoCallback; - class ListModel : public ModPlatform::ListModel { Q_OBJECT public: - ListModel(ModrinthPage* parent); - virtual ~ListModel(); - - public slots: - void searchRequestFinished(QJsonDocument& doc) override; + ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent){}; + virtual ~ListModel() = default; private: - const char** getSorts() const override; + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override { Modrinth::loadIndexedPack(m, obj); }; + + QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.object().value("hits").toArray(); }; + + static const char* sorts[5]; + const char** getSorts() const override { return sorts; }; }; } // namespace Modrinth From 9c57b54a81a9026aa86a983a203df17c023eaa8d Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 7 Mar 2022 19:29:59 -0300 Subject: [PATCH 068/605] refactor: move things around so that related things are close together This also adds some comments around ModModel.cpp and ModPage.cpp to add some ease of reading the code. Also move some things from headers to cpp files. --- launcher/ui/pages/modplatform/ModModel.cpp | 149 ++++++++++-------- launcher/ui/pages/modplatform/ModModel.h | 4 +- launcher/ui/pages/modplatform/ModPage.cpp | 61 +++++-- launcher/ui/pages/modplatform/ModPage.h | 6 +- .../pages/modplatform/flame/FlameModModel.cpp | 17 ++ .../pages/modplatform/flame/FlameModModel.h | 8 +- .../pages/modplatform/flame/FlameModPage.cpp | 42 +---- .../ui/pages/modplatform/flame/FlameModPage.h | 5 +- .../modplatform/modrinth/ModrinthModel.cpp | 19 ++- .../modplatform/modrinth/ModrinthModel.h | 10 +- .../modplatform/modrinth/ModrinthPage.cpp | 39 +---- .../pages/modplatform/modrinth/ModrinthPage.h | 5 +- 12 files changed, 200 insertions(+), 165 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index b8b90f84..6f3c6ce0 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -11,6 +11,24 @@ namespace ModPlatform { ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) {} +QString ListModel::debugName() const +{ + return m_parent->debugName(); +} + + +/******** Make data requests ********/ + +void ListModel::fetchMore(const QModelIndex& parent) +{ + if (parent.isValid()) return; + if (nextSearchOffset == 0) { + qWarning() << "fetchMore with 0 offset is wrong..."; + return; + } + performPaginatedSearch(); +} + QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); @@ -41,47 +59,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return QVariant(); } -QString ListModel::debugName() const -{ - return m_parent->debugName(); -} - -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) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } - } -} - -void ListModel::logoFailed(QString logo) -{ - m_failedLogos.append(logo); - m_loadingLogos.removeAll(logo); -} - -void ListModel::fetchMore(const QModelIndex& parent) -{ - if (parent.isValid()) return; - if (nextSearchOffset == 0) { - qWarning() << "fetchMore with 0 offset is wrong..."; - return; - } - performPaginatedSearch(); -} - -void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) -{ - if (m_logoMap.contains(logo)) { - callback(APPLICATION->metacache() - ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))) - ->getFullPath()); - } else { - requestLogo(logo, logoUrl); - } -} - void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) { m_parent->apiProvider()->getVersions(this, current.addonId.toString()); @@ -118,6 +95,60 @@ void ListModel::searchWithTerm(const QString& term, const int sort) performPaginatedSearch(); } +void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) +{ + if (m_logoMap.contains(logo)) { + callback(APPLICATION->metacache() + ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))) + ->getFullPath()); + } else { + requestLogo(logo, logoUrl); + } +} + +void ListModel::requestLogo(QString logo, QString url) +{ + if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; } + + MetaEntryPtr entry = + APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); + auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + job->deleteLater(); + emit logoLoaded(logo, QIcon(fullPath)); + if (waitingCallbacks.contains(logo)) { waitingCallbacks.value(logo)(fullPath); } + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + job->deleteLater(); + emit logoFailed(logo); + }); + + job->start(); + m_loadingLogos.append(logo); +} + + +/******** Request callbacks ********/ + +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) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } + } +} + +void ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + void ListModel::searchRequestFinished(QJsonDocument& doc) { jobPtr.reset(); @@ -175,32 +206,18 @@ void ListModel::searchRequestFailed(QString reason) void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) { - m_parent->onRequestVersionsSucceeded(doc, addonId); -} + auto& current = m_parent->getCurrent(); + if (addonId != current.addonId) { return; } + + QJsonArray arr = doc.array(); + try { + loadIndexedPackVersions(current, arr); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause(); + } -void ListModel::requestLogo(QString logo, QString url) -{ - if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; } - - MetaEntryPtr entry = - APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); - auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); - - auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { - job->deleteLater(); - emit logoLoaded(logo, QIcon(fullPath)); - if (waitingCallbacks.contains(logo)) { waitingCallbacks.value(logo)(fullPath); } - }); - - QObject::connect(job, &NetJob::failed, this, [this, logo, job] { - job->deleteLater(); - emit logoFailed(logo); - }); - - job->start(); - m_loadingLogos.append(logo); + m_parent->updateModVersions(); } } // namespace ModPlatform diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index e0cc098d..6c3ecc3d 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -36,6 +36,9 @@ class ListModel : public QAbstractListModel { void searchWithTerm(const QString& term, const int sort); void requestModVersions(const ModPlatform::IndexedPack& current); + virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; + virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0; + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); inline bool canFetchMore(const QModelIndex& parent) const override { return searchState == CanPossiblyFetchMore; }; @@ -54,7 +57,6 @@ class ListModel : public QAbstractListModel { void performPaginatedSearch(); protected: - virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; virtual QJsonArray documentToArray(QJsonDocument& obj) const = 0; virtual const char** getSorts() const = 0; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index a57f2903..94cf4bcf 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -3,6 +3,8 @@ #include +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) @@ -22,6 +24,9 @@ ModPage::~ModPage() delete ui; } + +/******** Qt things ********/ + void ModPage::openedImpl() { updateSelectionButton(); @@ -41,21 +46,8 @@ bool ModPage::eventFilter(QObject* watched, QEvent* event) return QWidget::eventFilter(watched, event); } -void ModPage::updateSelectionButton() -{ - if (!isOpened || selectedVersion < 0) { - ui->modSelectionButton->setEnabled(false); - return; - } - ui->modSelectionButton->setEnabled(true); - auto& version = current.versions[selectedVersion]; - if (!dialog->isModSelected(current.name, version.fileName)) { - ui->modSelectionButton->setText(tr("Select mod for download")); - } else { - ui->modSelectionButton->setText(tr("Deselect mod for download")); - } -} +/******** Callbacks to events in the UI (set up in the derived classes) ********/ void ModPage::triggerSearch() { @@ -130,3 +122,44 @@ void ModPage::onModSelected() updateSelectionButton(); } + + +/******** Make changes to the UI ********/ + +void ModPage::updateModVersions() +{ + auto packProfile = (static_cast(m_instance))->getPackProfile(); + + QString mcVersion = packProfile->getComponentVersion("net.minecraft"); + QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge"; + + for (int i = 0; i < current.versions.size(); i++) { + auto version = current.versions[i]; + //NOTE: Flame doesn't care about loaderString, so passing it changes nothing. + if (!validateVersion(version, mcVersion, loaderString)) { + continue; + } + ui->versionSelectionBox->addItem(version.version, QVariant(i)); + } + if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); } + + ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); + updateSelectionButton(); +} + + +void ModPage::updateSelectionButton() +{ + if (!isOpened || selectedVersion < 0) { + ui->modSelectionButton->setEnabled(false); + return; + } + + ui->modSelectionButton->setEnabled(true); + auto& version = current.versions[selectedVersion]; + if (!dialog->isModSelected(current.name, version.fileName)) { + ui->modSelectionButton->setText(tr("Select mod for download")); + } else { + ui->modSelectionButton->setText(tr("Deselect mod for download")); + } +} diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index aa0e8894..de66b3b0 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -35,9 +35,12 @@ class ModPage : public QWidget, public BasePage { virtual bool shouldDisplay() const override = 0; + virtual bool validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const = 0; + const ModAPI* apiProvider() const { return api.get(); }; - virtual void onRequestVersionsSucceeded(QJsonDocument&, QString) = 0; + ModPlatform::IndexedPack& getCurrent() { return current; } + void updateModVersions(); void openedImpl() override; bool eventFilter(QObject* watched, QEvent* event) override; @@ -45,7 +48,6 @@ class ModPage : public QWidget, public BasePage { BaseInstance* m_instance; protected: - void updateSelectionButton(); protected slots: diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index 7588a714..ce2f74f1 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -1,7 +1,24 @@ #include "FlameModModel.h" +#include "modplatform/flame/FlameModIndex.h" + namespace FlameMod { const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; +void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadIndexedPack(m, obj); +}; + +void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ + FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); +}; + +QJsonArray ListModel::documentToArray(QJsonDocument& obj) const +{ + return obj.array(); +} + } // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h index 204834c9..cf3042ed 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h @@ -1,7 +1,6 @@ #pragma once #include "FlameModPage.h" -#include "modplatform/flame/FlameModIndex.h" namespace FlameMod { @@ -14,12 +13,13 @@ class ListModel : public ModPlatform::ListModel { virtual ~ListModel() = default; private: - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override { FlameMod::loadIndexedPack(m, obj); }; + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.array(); }; + QJsonArray documentToArray(QJsonDocument& obj) const override; static const char* sorts[6]; - const char** getSorts() const override { return sorts; }; + inline const char** getSorts() const override { return sorts; }; }; } // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 19f58280..091e49c7 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -1,15 +1,7 @@ #include "FlameModPage.h" #include "ui_ModPage.h" -#include - -#include "Application.h" #include "FlameModModel.h" -#include "InstanceImportTask.h" -#include "Json.h" -#include "ModDownloadTask.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) @@ -34,31 +26,13 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected); } -bool FlameModPage::shouldDisplay() const { return true; } - -void FlameModPage::onRequestVersionsSucceeded(QJsonDocument& doc, QString addonId) +bool FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const { - if (addonId != current.addonId) { - return; // wrong request - } - - QJsonArray arr = doc.array(); - try { - FlameMod::loadIndexedPackVersions(current, arr, APPLICATION->network(), m_instance); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading Flame mod version: " << e.cause(); - } - auto packProfile = ((MinecraftInstance*)m_instance)->getPackProfile(); - QString mcVersion = packProfile->getComponentVersion("net.minecraft"); - QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge"; - for (int i = 0; i < current.versions.size(); i++) { - auto version = current.versions[i]; - if (!version.mcVersion.contains(mcVersion)) { continue; } - ui->versionSelectionBox->addItem(version.version, QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No Valid Version found!"), QVariant(-1)); } - - ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); - updateSelectionButton(); + (void) loaderVer; + return ver.mcVersion.contains(mineVer); } + +// 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... +bool FlameModPage::shouldDisplay() const { return true; } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index f15b51ec..90513ca5 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -19,8 +19,7 @@ class FlameModPage : public ModPage { inline QString debugName() const override { return tr("Flame"); } inline QString metaEntryBase() const override { return "FlameMods"; }; - bool shouldDisplay() const override; + bool validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const override; - private: - void onRequestVersionsSucceeded(QJsonDocument&, QString) override; + bool shouldDisplay() const override; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 9361546e..0db6aeff 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -1,7 +1,24 @@ #include "ModrinthModel.h" +#include "modplatform/modrinth/ModrinthPackIndex.h" + namespace Modrinth { -const char* ListModel::sorts[5] { "relevance", "downloads", "follows", "updated", "newest" }; +const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; + +void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + Modrinth::loadIndexedPack(m, obj); +} + +void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ + Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); +}; + +QJsonArray ListModel::documentToArray(QJsonDocument& obj) const +{ + return obj.object().value("hits").toArray(); +} } // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 9137190d..de73704c 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -1,7 +1,6 @@ #pragma once #include "ModrinthPage.h" -#include "modplatform/modrinth/ModrinthPackIndex.h" namespace Modrinth { @@ -13,12 +12,13 @@ class ListModel : public ModPlatform::ListModel { virtual ~ListModel() = default; private: - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override { Modrinth::loadIndexedPack(m, obj); }; - - QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.object().value("hits").toArray(); }; + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + + QJsonArray documentToArray(QJsonDocument& obj) const override; static const char* sorts[5]; - const char** getSorts() const override { return sorts; }; + inline const char** getSorts() const override { return sorts; }; }; } // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index fa703e38..cf01506e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -1,15 +1,7 @@ #include "ModrinthPage.h" #include "ui_ModPage.h" -#include - -#include "Application.h" -#include "InstanceImportTask.h" -#include "Json.h" -#include "ModDownloadTask.h" #include "ModrinthModel.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) @@ -33,29 +25,12 @@ ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthPage::onModSelected); } -bool ModrinthPage::shouldDisplay() const { return true; } - -void ModrinthPage::onRequestVersionsSucceeded(QJsonDocument& response, QString addonId) +bool ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const { - if (addonId != current.addonId) { return; } - - QJsonArray arr = response.array(); - try { - Modrinth::loadIndexedPackVersions(current, arr, APPLICATION->network(), m_instance); - } catch (const JSONValidationError& e) { - qDebug() << response; - qWarning() << "Error while reading Modrinth mod version: " << e.cause(); - } - auto packProfile = ((MinecraftInstance*)m_instance)->getPackProfile(); - QString mcVersion = packProfile->getComponentVersion("net.minecraft"); - QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge"; - for (int i = 0; i < current.versions.size(); i++) { - auto version = current.versions[i]; - if (!version.mcVersion.contains(mcVersion) || !version.loaders.contains(loaderString)) { continue; } - ui->versionSelectionBox->addItem(version.version, QVariant(i)); - } - if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No Valid Version found !"), QVariant(-1)); } - - ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); - updateSelectionButton(); + return ver.mcVersion.contains(mineVer) && ver.loaders.contains(loaderVer); } + +// 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... +bool ModrinthPage::shouldDisplay() const { return true; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index f6d1eef0..6f387708 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -19,8 +19,7 @@ class ModrinthPage : public ModPage { inline QString debugName() const override { return tr("Modrinth"); } inline QString metaEntryBase() const override { return "ModrinthPacks"; }; - bool shouldDisplay() const override; + bool validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const override; - private: - void onRequestVersionsSucceeded(QJsonDocument&, QString) override; + bool shouldDisplay() const override; }; From b3c2a56ece925bc6fe327b8824c50f194610b5b9 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 7 Mar 2022 19:55:20 -0300 Subject: [PATCH 069/605] fix: delete semicolons at the end of .cpp file's functions my lsp is weird sometimes --- launcher/modplatform/helpers/NetworkModAPI.cpp | 4 ++-- launcher/ui/pages/modplatform/flame/FlameModModel.cpp | 4 ++-- launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp index 4b066102..ef084535 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.cpp +++ b/launcher/modplatform/helpers/NetworkModAPI.cpp @@ -29,7 +29,7 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const }); netJob->start(); -}; +} void NetworkModAPI::getVersions(CallerType* caller, const QString& addonId) const { @@ -57,4 +57,4 @@ void NetworkModAPI::getVersions(CallerType* caller, const QString& addonId) cons }); netJob->start(); -}; +} diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index ce2f74f1..8437f623 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -9,12 +9,12 @@ const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name" void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) { FlameMod::loadIndexedPack(m, obj); -}; +} void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); -}; +} QJsonArray ListModel::documentToArray(QJsonDocument& obj) const { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 0db6aeff..ac3c14f2 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -14,7 +14,7 @@ void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); -}; +} QJsonArray ListModel::documentToArray(QJsonDocument& obj) const { From 4fe13c64a15137c22a72f314a041c647ec266175 Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Tue, 8 Mar 2022 19:07:21 +1000 Subject: [PATCH 070/605] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d83a8f9..30b87510 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ If you want to contribute to PolyMC you might find it useful to join our Discord ## Building -If you want to build PolyMC yourself, check [BUILD.md](BUILD.md) for build instructions. +If you want to build PolyMC yourself, check [Build Instructions](https://polymc.org/wiki/development/build-instructions/) for build instructions. ## Code formatting @@ -72,3 +72,9 @@ To modify download infomation or change packaging infomation send a pull request ## Forking/Redistributing/Custom builds policy Do whatever you want, we don't care. Just follow the license. If you have any questions about this feel free to ask in an issue. + +All launcher code is available under the GPL-3 license. + +[Source for the website](https://github.com/PolyMC/polymc.github.io) is hosted under the AGPL-3 License. + +The logo and related assets are under the CC BY-NC-SA 4.0 license. From 0813eba3675e8c9fcfc6962377732b70bb62b1bb Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Tue, 8 Mar 2022 19:09:54 +1000 Subject: [PATCH 071/605] Update BUILD.md --- BUILD.md | 340 +------------------------------------------------------ 1 file changed, 2 insertions(+), 338 deletions(-) diff --git a/BUILD.md b/BUILD.md index fa6a8d04..d53fb7a7 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,341 +1,5 @@ # Build Instructions -# Contents +Build instructions are available on [the website](https://polymc.org/wiki/development/build-instructions/). -- [Getting the source](#getting-the-source) -- [Linux](#linux) -- [Windows](#windows) -- [macOS](#macos) - -# Getting the source - -Clone the source code using git and grab all the submodules: - -``` -git clone https://github.com/PolyMC/PolyMC.git -cd PolyMC -git submodule init -git submodule update -``` - -The rest of the documentation assumes you have already cloned the repository. - -# Linux and FreeBSD - -Getting the project to build and run on Linux is easy if you use any modern and up-to-date linux distribution. If you're using FreeBSD you should use 13.0-RELEASE or newer. - -## Build dependencies -- A C++ compiler capable of building C++11 code. -- Qt Development tools 5.6 or newer (`qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5` on Debian-based system) -- cmake 3.1 or newer (`cmake` on Debian-based system) -- zlib (`zlib1g-dev` on Debian-based system) -- Java JDK (`openjdk-17-jdk`on Debian-based system) -- GL headers (`libgl1-mesa-dev` on Debian-based system) -- games/lwjgl port if using FreeBSD - -You can use IDEs like KDevelop or QtCreator to open the CMake project if you want to work on the code. - -### Building a portable binary - -```sh -mkdir install -# configure the project -cmake -S . -B build \ - -DCMAKE_INSTALL_PREFIX=./install -# build -cd build -make -j$(nproc) install -``` - -### Building & Installing to the System - -This is the preferred method for installation, and is suitable for packages. - -```sh -# configure everything -cmake -S . -B build \ -  -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX="/usr" \ # Use "/usr" when building Linux packages. If building on FreeBSD or not for package, use "/usr/local" - -DLauncher_PORTABLE=OFF -cd build -make -j$(nproc) install # Optionally specify DESTDIR for packages (i.e. DESTDIR=${pkgdir}) -``` - -### Building a .deb - -Requirements: [makedeb](https://docs.makedeb.org/) installed on your system. - -``` -git clone https://mpr.makedeb.org/polymc.git -cd polymc -makedeb -s -``` - -The deb will be located in the directory the repo was cloned in. - -### Building an .rpm - -Build dependencies are automatically installed using `dnf`, but you do need the `rpmdevtools` package (on Fedora) -in order to fetch sources and setup your tree. -You don't need to clone the repo for this; the spec file handles that - -``` -cd ~ -# setup your ~/rpmbuild directory, required for rpmbuild to work. -rpmdev-setuptree -# get the rpm spec file from the polymc-misc repo -wget https://raw.githubusercontent.com/PolyMC/polymc-misc/master/rpm/polymc.spec -# install build dependencies -sudo dnf builddep polymc.spec -# download build sources -spectool -g -R polymc.spec -# now build! -rpmbuild -bb polymc.spec -``` - -The path to the rpm packages will be printed when the build is complete. - -### Building a Slackware package - -To build a Slackware package, first install [qt5 SlackBuild](http://slackbuilds.org/repository/14.2/libraries/qt5/) (on 15.0 and newer installed by defualt), then set up a [JDK](https://codeberg.org/glowiak/SlackBuilds/raw/branch/master/tgz/adoptium-jdk8.tar.gz). - -If you're using Slackware 14.2, update cmake with these commands: - -``` -mkdir -p /tmp/SBo -cd /tmp/SBo -wget -c https://github.com/Kitware/CMake/releases/download/v3.22.2/cmake-3.22.2.tar.gz -tar xzvf cmake-3.22.2.tar.gz -cd cmake-3.22.2 -./configure --prefix=/usr -make -sudo make install -``` - -Next, download the [SlackBuild](https://codeberg.org/glowiak/SlackBuilds/raw/branch/master/tgz/polymc.tar.gz), unpack it and type in extracted directory: - -``` -sudo ./polymc.SlackBuild # script will do everything, just sit up and wait -sudo /sbin/installpkg /tmp/polymc-version-arch-1_SBo.tgz # install the created package -``` - -### Building a flatpak - -You don't need to clone the entire PolyMC repo for this; the flatpak file handles that. -`flatpak` and `flatpak-builder` need to be installed on your system - -```sh -git clone https://github.com/flathub/org.polymc.PolyMC -cd org.polymc.PolyMC -# remove --user --install if you want to build without installing -flatpak-builder --user --install flatbuild org.polymc.PolyMC.yml -``` - -### Installing Qt using the installer (optional) - -1. Run the Qt installer. -2. Choose a place to install Qt. -3. Choose the components you want to install. - - You need Qt 5.6.x 64-bit ticked. - - You need Tools/Qt Creator ticked. - - Other components are selected by default, you can untick them if you don't need them. -4. Accept the license agreements. -5. Double check the install details and then click "Install". - - Installation can take a very long time, go grab a cup of tea or something and let it work. - -### Loading the project in Qt Creator (optional) - -1. Open Qt Creator. -2. Choose `File->Open File or Project`. -3. Navigate to the Launcher source folder you cloned and choose CMakeLists.txt. -4. Read the instructions that just popped up about a build location and choose one. -5. You should see "Run CMake" in the window. - - Make sure that Generator is set to "Unix Generator (Desktop Qt 5.6.x GCC 64bit)". - - Hit the "Run CMake" button. - - You'll see warnings and it might not be clear that it succeeded until you scroll to the bottom of the window. - - Hit "Finish" if CMake ran successfully. -6. Cross your fingers and press the Run button (bottom left of Qt Creator). - - If the project builds successfully it will run and the Launcher window will pop up. - -**If this doesn't work for you, let us know on our Discord.** - -# Windows - -Getting the project to build and run on Windows is easy if you use Qt's IDE, Qt Creator. The project will simply not compile using Microsoft build tools, because that's not something we do. If it does compile, it is by chance only. - -## Dependencies - -- [Qt 5.6+ Development tools](http://qt-project.org/downloads) -- Qt Online Installer for Windows - - http://download.qt.io/new_archive/qt/5.6/5.6.0/qt-opensource-windows-x86-mingw492-5.6.0.exe - - Download the MinGW version (MSVC version does not work). -- [OpenSSL](https://github.com/IndySockets/OpenSSL-Binaries/tree/master/Archive/) -- Win32 OpenSSL, version 1.0.2g (from 2016) - - https://github.com/IndySockets/OpenSSL-Binaries/raw/master/Archive/openssl-1.0.2g-i386-win32.zip - - the usual OpenSSL for Windows (http://slproweb.com/products/Win32OpenSSL.html) only provides the newest version of OpenSSL, and we need the 1.0.2g version - - **Download the 32-bit version, not 64-bit.** - - Microsoft Visual C++ 2008 Redist is required for this, there's a link on the OpenSSL download page above next to the main download. - - We use a custom build of OpenSSL that doesn't have this dependency. For normal development, the custom build is not necessary though. -- [zlib 1.2+](http://gnuwin32.sourceforge.net/packages/zlib.htm) - the Setup is fine -- [Java JDK 8](https://adoptium.net/releases.html?variant=openjdk8) - Use the MSI installer. -- [CMake](http://www.cmake.org/cmake/resources/software.html) -- Windows (Win32 Installer) - -Ensure that OpenSSL, zlib, Java and CMake are on `PATH`. - -## Getting set up - -### Installing Qt - -1. Run the Qt installer -2. Choose a place to install Qt (C:\Qt is the default), -3. Choose the components you want to install - - You need Qt 5.6 (32 bit) ticked, - - You need Tools/Qt Creator ticked, - - Other components are selected by default, you can untick them if you don't need them. -4. Accept the license agreements, -5. Double check the install details and then click "Install" - - Installation can take a very long time, go grab a cup of tea or something and let it work. - -### Installing OpenSSL - -1. Download .zip file from the link above. -2. Unzip and add the directory to PATH, so CMake can find it. - -### Installing CMake - -1. Run the CMake installer, -2. It's easiest if you choose to add CMake to the PATH for all users, - - If you don't choose to do this, remember where you installed CMake. - -### Loading the project - -1. Open Qt Creator, -2. Choose File->Open File or Project, -3. Navigate to the Launcher source folder you cloned and choose CMakeLists.txt, -4. Read the instructions that just popped up about a build location and choose one, -5. If you chose not to add CMake to the system PATH, tell Qt Creator where you installed it, - - Otherwise you can skip this step. -6. You should see "Run CMake" in the window, - - Make sure that Generator is set to "MinGW Generator (Desktop Qt 5.6.x MinGW 32bit)", - - Hit the "Run CMake" button, - - You'll see warnings and it might not be clear that it succeeded until you scroll to the bottom of the window. - - Hit "Finish" if CMake ran successfully. -7. Cross your fingers and press the Run button (bottom left of Qt Creator)! - - If the project builds successfully it will run and the Launcher window will pop up, - - Test OpenSSL by making an instance and trying to log in. If Qt Creator couldn't find OpenSSL during the CMake stage, login will fail and you'll get an error. - -The following .dlls are needed for the app to run (copy them to build directory if you want to be able to move the build to another pc): - -``` - -platforms/qwindows.dll -libeay32.dll -libgcc_s_dw2-1.dll -libssp-0.dll -libstdc++-6.dll -libwinpthread-1.dll -Qt5Core.dll -Qt5Gui.dll -Qt5Network.dll -Qt5Svg.dll -Qt5Widgets.dll -Qt5Xml.dll -ssleay32.dll -zlib1.dll - -``` - -**These build instructions worked for me (Drayshak) on a fresh Windows 8 x64 Professional install. If they don't work for you, let us know on our Discord.** - -### Compile from command line on Windows - -1. If you installed Qt with the web installer, there should be a shortcut called `Qt 5.4 for Desktop (MinGW 4.9 32-bit)` in the Start menu on Windows 7 and 10. Best way to find it is to search for it. Do note you cannot just use cmd.exe, you have to use the shortcut, otherwise the proper MinGW software will not be on the PATH. -2. Once that is open, change into your user directory, and clone PolyMC by doing `git clone --recursive https://github.com/PolyMC/PolyMC.git`, and change directory to the folder you cloned to. -3. Make a build directory, and change directory to the directory and do `cmake -G "MinGW Makefiles" -DCMAKE_INSTALL_PREFIX=C:\Path\that\makes\sense\for\you`. By default, it will install to C:\Program Files (x86), which you might not want, if you want a local installation. If you want to install it to that directory, make sure to run the command window as administrator. -4. If you want PolyMC to store its data in `%APPDATA%`, append `-DLauncher_PORTABLE=OFF` to the previous command. -5. Do `mingw32-make -j$(nproc)`, where X is the number of cores your CPU has plus one. -6. Now to wait for it to compile. This could take some time. Hopefully it compiles properly. -7. Run the command `mingw32-make install`, and it should install PolyMC, to whatever the `-DCMAKE_INSTALL_PREFIX` was. -8. In most cases, whenever compiling, the OpenSSL dll's aren't put into the directory to where PolyMC installs, meaning you cannot log in. The best way to fix this is just to do `copy C:\OpenSSL-Win32\*.dll C:\Where\you\installed\PolyMC\to`. This should copy the required OpenSSL dll's to log in. - -# macOS - -### Install prerequisites: - -- Install XCode Command Line tools -- Install the official build of CMake (https://cmake.org/download/) -- Install JDK 8 (https://adoptium.net/releases.html?variant=openjdk8&jvmVariant=hotspot) -- Get Qt 5.6 and install it (https://download.qt.io/new_archive/qt/5.6/5.6.3/) or higher (tested) (https://www.qt.io/download-qt-installer?utm_referrer=https%3A%2F%2Fwww.qt.io%2Fdownload-open-source) - -You can use `homebrew` to simplify the installation of build dependencies - -### XCode Command Line tools - -If you don't have XCode CommandLine tools installed, you can install them by using this command in the Terminal App - -```bash -xcode-select --install -``` - -### Build - -Pick an installation path - this is where the final `PolyMC.app` will be constructed when you run `make install`. Supply it as the `CMAKE_INSTALL_PREFIX` argument during CMake configuration. By default, it's in the dist folder under PolyMC - -``` -mkdir build -cd build -cmake \ - -DCMAKE_C_COMPILER=/usr/bin/clang \ - -DCMAKE_CXX_COMPILER=/usr/bin/clang++ \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX:PATH="$(dirname $PWD)/dist/" \ - -DCMAKE_PREFIX_PATH="/path/to/Qt/" \ - -DQt5_DIR="/path/to/Qt/" \ - -DCMAKE_OSX_DEPLOYMENT_TARGET=10.7 \ - .. -make install -``` - -Remember to replace `/path/to/Qt/` with the actual path. For newer Qt installations, it is often in your home directory. - -**Note:** The final app bundle may not run due to code signing issues, which -need to be fixed with `codesign -fs -`. - -# OpenBSD - -Tested on OpenBSD 7.0-alpha i386, on older should work too - -## Build dependencies -- A C++ compiler capable of building C++11 code (included in base system) -- Qt Development tools 5.6 or newer ([meta/qt5](https://openports.se/meta/qt5)) -- cmake 3.1 or newer ([devel/cmake](https://openports.se/devel/cmake)) -- zlib (included in base system) -- Java JDK ([devel/jdk-1.8](https://openports.se/devel/jdk/1.8)) -- GL headers (included in base system) -- lwjgl ([games/lwjgl](https://openports.se/games/lwjgl) and [games/lwjgl3](https://openports.se/games/lwjgl3)) - -You can use IDEs like KDevelop or QtCreator to open the CMake project if you want to work on the code. - -### Building a portable binary - -```sh -mkdir install -# configure the project -cmake -S . -B build \ - -DCMAKE_INSTALL_PREFIX=./install -DCMAKE_PREFIX_PATH=/usr/local/lib/qt5/cmake -# build -cd build -make -j$(nproc) install -``` - -### Building & Installing to the System - -This is the preferred method for installation, and is suitable for packages. - -```sh -# configure everything -cmake -S . -B build \ -  -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX="/usr/local" \ # /usr/local is default in OpenBSD and FreeBSD - -DLauncher_PORTABLE=OFF -DCMAKE_PREFIX_PATH=/usr/local/lib/qt5/cmake # use linux layout and point to qt5 libs -cd build -make -j$(nproc) install # Optionally specify DESTDIR for packages (i.e. DESTDIR=${pkgdir}) -``` +If you would like to contribute or fix an issue with the Build instructions your can do so [here](https://github.com/PolyMC/polymc.github.io/blob/master/src/wiki/development/build-instructions.md) From 49d122a2c47fdf7aed04dfb950b2333f60ba855c Mon Sep 17 00:00:00 2001 From: deadmeu Date: Tue, 8 Mar 2022 23:24:11 +1000 Subject: [PATCH 072/605] Fix missing space in "No Accounts" message --- launcher/LaunchController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 40178b70..4fd2eade 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -56,7 +56,7 @@ void LaunchController::decideAccount() m_parentWidget, tr("No Accounts"), tr("In order to play Minecraft, you must have at least one Mojang or Minecraft " - "account logged in." + "account logged in. " "Would you like to open the account manager to add an account now?"), QMessageBox::Information, QMessageBox::Yes | QMessageBox::No From d814e21f0d68b560d94cd69edb0b673a1507aa63 Mon Sep 17 00:00:00 2001 From: dada513 Date: Tue, 8 Mar 2022 18:41:23 +0100 Subject: [PATCH 073/605] add matrix button --- CMakeLists.txt | 5 +++++ buildconfig/BuildConfig.cpp.in | 1 + buildconfig/BuildConfig.h | 1 + launcher/resources/multimc/multimc.qrc | 4 ++++ launcher/resources/multimc/scalable/matrix.svg | 7 +++++++ launcher/ui/MainWindow.cpp | 18 +++++++++++++++++- launcher/ui/MainWindow.h | 2 ++ 7 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 launcher/resources/multimc/scalable/matrix.svg diff --git a/CMakeLists.txt b/CMakeLists.txt index e2545ba5..e633eb55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,9 +82,14 @@ set(Launcher_BUG_TRACKER_URL "https://github.com/PolyMC/PolyMC/issues" CACHE STR # Translations Platform URL set(Launcher_TRANSLATIONS_URL "https://hosted.weblate.org/projects/polymc/polymc/" CACHE STRING "URL for the translations platform.") +# Matrix Space +set(Launcher_MATRIX_URL "https://matrix.to/#/#polymc:polymc.org" CACHE STRING "URL to the Matrix Space") + # Discord URL set(Launcher_DISCORD_URL "https://discord.gg/Z52pwxWCHP" CACHE STRING "URL for the Discord guild.") + + # Subreddit URL set(Launcher_SUBREDDIT_URL "" CACHE STRING "URL for the subreddit.") diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 80c67037..d1bd4d05 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -57,6 +57,7 @@ Config::Config() BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@"; TRANSLATIONS_URL = "@Launcher_TRANSLATIONS_URL@"; + MATRIX_URL = "@Launcher_MATRIX_URL@"; DISCORD_URL = "@Launcher_DISCORD_URL@"; SUBREDDIT_URL = "@Launcher_SUBREDDIT_URL@"; } diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 95619587..cf74b39e 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -95,6 +95,7 @@ public: QString BUG_TRACKER_URL; QString TRANSLATIONS_URL; + QString MATRIX_URL; QString DISCORD_URL; QString SUBREDDIT_URL; diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index ef29cf9b..d31885b9 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -246,9 +246,13 @@ scalable/screenshot-placeholder.svg + + scalable/matrix.svg + scalable/discord.svg + 32x32/instances/chicken.png 128x128/instances/chicken.png diff --git a/launcher/resources/multimc/scalable/matrix.svg b/launcher/resources/multimc/scalable/matrix.svg new file mode 100644 index 00000000..237c55a2 --- /dev/null +++ b/launcher/resources/multimc/scalable/matrix.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index ad7227cc..2cb3c7fd 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -235,6 +235,7 @@ public: TranslatedToolButton helpMenuButton; TranslatedAction actionReportBug; TranslatedAction actionDISCORD; + TranslatedAction actionMATRIX; TranslatedAction actionREDDIT; TranslatedAction actionAbout; @@ -343,13 +344,23 @@ public: all_actions.append(&actionReportBug); helpMenu->addAction(actionReportBug); } + + if(!BuildConfig.MATRIX_URL.isEmpty()) { + actionMATRIX = TranslatedAction(MainWindow); + actionMATRIX->setObjectName(QStringLiteral("actionMATRIX")); + actionMATRIX->setIcon(APPLICATION->getThemedIcon("matrix")); + actionMATRIX.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Matrix")); + actionMATRIX.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Matrix space")); + all_actions.append(&actionMATRIX); + helpMenu->addAction(actionMATRIX); + } if (!BuildConfig.DISCORD_URL.isEmpty()) { actionDISCORD = TranslatedAction(MainWindow); actionDISCORD->setObjectName(QStringLiteral("actionDISCORD")); actionDISCORD->setIcon(APPLICATION->getThemedIcon("discord")); actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Discord")); - actionDISCORD.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 discord voice chat.")); + actionDISCORD.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Discord guild.")); all_actions.append(&actionDISCORD); helpMenu->addAction(actionDISCORD); } @@ -1500,6 +1511,11 @@ void MainWindow::on_actionDISCORD_triggered() DesktopServices::openUrl(QUrl(BuildConfig.DISCORD_URL)); } +void MainWindow::on_actionMATRIX_triggered() +{ + DesktopServices::openUrl(QUrl(BuildConfig.MATRIX_URL)); +} + void MainWindow::on_actionChangeInstIcon_triggered() { if (!m_selectedInstance) diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index f6940ab0..bdbe81aa 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -73,6 +73,8 @@ private slots: void on_actionREDDIT_triggered(); + void on_actionMATRIX_triggered(); + void on_actionDISCORD_triggered(); void on_actionCopyInstance_triggered(); From c1201997a3d753e364db5599c25554df803896ae Mon Sep 17 00:00:00 2001 From: txtsd Date: Thu, 10 Mar 2022 13:38:16 +0530 Subject: [PATCH 074/605] Fix Ubuntu system Qt failure --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index af5a1074..b5797e95 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -100,6 +100,7 @@ jobs: - name: Install System Qt on Linux if: runner.os == 'Linux' && matrix.app_image != true run: | + sudo apt-get -y update sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 - name: Install Ninja From c799faaca6e0d57364cb9509be4b1a842e4398ee Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 11 Mar 2022 09:21:59 +0100 Subject: [PATCH 075/605] fix: typos --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index d53fb7a7..8a76b68b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -2,4 +2,4 @@ Build instructions are available on [the website](https://polymc.org/wiki/development/build-instructions/). -If you would like to contribute or fix an issue with the Build instructions your can do so [here](https://github.com/PolyMC/polymc.github.io/blob/master/src/wiki/development/build-instructions.md) +If you would like to contribute or fix an issue with the Build instructions you can do so [here](https://github.com/PolyMC/polymc.github.io/blob/master/src/wiki/development/build-instructions.md). From a3d7ad731da927cc8cc856f6335b4aec22689db5 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 11 Mar 2022 18:43:17 -0300 Subject: [PATCH 076/605] fix missing translation strings my mistake, sorry! ToT --- launcher/ui/pages/instance/ModFolderPage.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index e4ad9012..ffb87bbe 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -152,8 +152,8 @@ ModFolderPage::ModFolderPage( ui->actionsToolbar->insertActionBefore(ui->actionAdd, act); connect(act, &QAction::triggered, this, &ModFolderPage::on_actionInstall_mods_triggered); - ui->actionAdd->setText("Add .jar"); - ui->actionAdd->setToolTip("Add mods via local file"); + ui->actionAdd->setText(tr("Add .jar")); + ui->actionAdd->setToolTip(tr("Add mods via local file")); } ui->actionsToolbar->insertSpacer(ui->actionView_configs); From 641a96e4a9bfe70aeec0c58ec5b81a2027c71a61 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 12 Mar 2022 18:17:25 +0100 Subject: [PATCH 077/605] fix(metainfo): update URLs --- program_info/org.polymc.PolyMC.metainfo.xml.in | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/program_info/org.polymc.PolyMC.metainfo.xml.in b/program_info/org.polymc.PolyMC.metainfo.xml.in index 03401f3d..7972965c 100644 --- a/program_info/org.polymc.PolyMC.metainfo.xml.in +++ b/program_info/org.polymc.PolyMC.metainfo.xml.in @@ -11,7 +11,7 @@ CC0-1.0 GPL-3.0-or-later https://polymc.org/ - https://github.com/PolyMC/PolyMC#help--support + https://polymc.org/wiki/

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

Features:

@@ -28,23 +28,23 @@ The main PolyMC window - https://polymc.github.io/assets/img/screenshots/LauncherDark.png + https://polymc.org/img/screenshots/LauncherDark.png Modpack installation - https://polymc.github.io/assets/img/screenshots/ModpackInstallDark.png + https://polymc.org/img/screenshots/ModpackInstallDark.png Mod installation - https://polymc.github.io/assets/img/screenshots/ModInstallDark.png + https://polymc.org/img/screenshots/ModInstallDark.png Instance management - https://polymc.github.io/assets/img/screenshots/PropertiesDark.png + https://polymc.org/img/screenshots/PropertiesDark.png Cat :) - https://polymc.github.io/assets/img/screenshots/LauncherCatDark.png + https://polymc.org/img/screenshots/LauncherCatDark.png From bb2b344d3300cfc2f13211cd12956e23c98459de Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 13 Mar 2022 11:44:30 +0100 Subject: [PATCH 078/605] fix: define jars path relative to application root Fixes #117 --- CMakeLists.txt | 3 ++- launcher/Application.cpp | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5250d0ff..6cabab7c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -207,7 +207,8 @@ elseif(UNIX) set(LAUNCHER_METAINFO_DEST_DIR "share/metainfo" CACHE STRING "Path to the metainfo directory") set(LAUNCHER_ICON_DEST_DIR "share/icons/hicolor/scalable/apps" CACHE STRING "Path to the scalable icon directory") - set(Launcher_APP_BINARY_DEFS "-DMULTIMC_JARS_LOCATION=${CMAKE_INSTALL_PREFIX}/${JARS_DEST_DIR}") + # jars path is determined on runtime, relative to "Application root path", generally /usr for Launcher_PORTABLE=0 + set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_JARS_LOCATION=${JARS_DEST_DIR}") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${LAUNCHER_DESKTOP_DEST_DIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${LAUNCHER_METAINFO_DEST_DIR}) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 0ce80d4b..c699d840 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -515,8 +515,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) FS::updateTimestamp(m_rootPath); #endif -#ifdef MULTIMC_JARS_LOCATION - m_jarsPath = TOSTRING(MULTIMC_JARS_LOCATION); +#ifdef LAUNCHER_JARS_LOCATION + m_jarsPath = TOSTRING(LAUNCHER_JARS_LOCATION); #endif qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT; @@ -1494,7 +1494,7 @@ QString Application::getJarsPath() { return FS::PathCombine(QCoreApplication::applicationDirPath(), "jars"); } - return m_jarsPath; + return FS::PathCombine(m_rootPath, m_jarsPath); } QString Application::getMSAClientID() From 84a142096f205c938ce7a561d029d6c2c4f2cda2 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 11 Mar 2022 17:09:44 +0100 Subject: [PATCH 079/605] chore: switch logo font to Josefin Sans --- program_info/polymc-header-black.svg | 37 ++++--- program_info/polymc-header.Source.svg | 139 ++++++++++++++++++++++++++ program_info/polymc-header.svg | 37 ++++--- 3 files changed, 175 insertions(+), 38 deletions(-) create mode 100644 program_info/polymc-header.Source.svg diff --git a/program_info/polymc-header-black.svg b/program_info/polymc-header-black.svg index 34cda4ab..e93be00c 100644 --- a/program_info/polymc-header-black.svg +++ b/program_info/polymc-header-black.svg @@ -1,31 +1,30 @@ - - + - - - - - - - - - - + + + + + + + + - - - - - - - + + + + + + + + + diff --git a/program_info/polymc-header.Source.svg b/program_info/polymc-header.Source.svg new file mode 100644 index 00000000..c960f33b --- /dev/null +++ b/program_info/polymc-header.Source.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + PolyMC + + + + diff --git a/program_info/polymc-header.svg b/program_info/polymc-header.svg index a896787f..bfad7215 100644 --- a/program_info/polymc-header.svg +++ b/program_info/polymc-header.svg @@ -1,31 +1,30 @@ - - + - - - - - - - - - - + + + + + + + + - - - - - - - + + + + + + + + + From f1c21a912a180cd542f5789f623bf50617f41387 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 11 Mar 2022 17:44:54 +0100 Subject: [PATCH 080/605] fix: simplify header SVG using SVGO --- program_info/polymc-header-black.svg | 33 +++++++++++----------------- program_info/polymc-header.svg | 33 +++++++++++----------------- 2 files changed, 26 insertions(+), 40 deletions(-) diff --git a/program_info/polymc-header-black.svg b/program_info/polymc-header-black.svg index e93be00c..e9e7c3e2 100644 --- a/program_info/polymc-header-black.svg +++ b/program_info/polymc-header-black.svg @@ -1,30 +1,23 @@ - + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/program_info/polymc-header.svg b/program_info/polymc-header.svg index bfad7215..837004e1 100644 --- a/program_info/polymc-header.svg +++ b/program_info/polymc-header.svg @@ -1,30 +1,23 @@ - + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + From 71a4333f5570fcbd8dc9d8937f44a40cc66ab6ac Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Sun, 13 Mar 2022 23:25:17 +1000 Subject: [PATCH 081/605] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 30b87510..a114869c 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,8 @@ In general, in order of importance: The translation effort for PolyMC is hosted on [Weblate](https://hosted.weblate.org/projects/polymc/polymc/) and information about translating PolyMC is available at https://github.com/PolyMC/Translations -## Download infomation -To modify download infomation or change packaging infomation send a pull request or issue to the website [Here](https://github.com/PolyMC/polymc.github.io/blob/master/src/download.md) +## Download information +To modify download information or change packaging information send a pull request or issue to the website [Here](https://github.com/PolyMC/polymc.github.io/blob/master/src/download.md) ## Forking/Redistributing/Custom builds policy From b3b613d8b4a5012d22b9e1646f4367fd27c783e3 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 13 Mar 2022 11:50:18 -0300 Subject: [PATCH 082/605] feat(ui): make a better "Mod download confirmation dialog" --- launcher/CMakeLists.txt | 3 + launcher/ui/dialogs/ModDownloadDialog.cpp | 27 ++----- launcher/ui/dialogs/ReviewMessageBox.cpp | 31 +++++++ launcher/ui/dialogs/ReviewMessageBox.h | 23 ++++++ launcher/ui/dialogs/ReviewMessageBox.ui | 98 +++++++++++++++++++++++ 5 files changed, 163 insertions(+), 19 deletions(-) create mode 100644 launcher/ui/dialogs/ReviewMessageBox.cpp create mode 100644 launcher/ui/dialogs/ReviewMessageBox.h create mode 100644 launcher/ui/dialogs/ReviewMessageBox.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 86c05651..039841ef 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -797,6 +797,8 @@ SET(LAUNCHER_SOURCES ui/pagedialog/PageDialog.h ui/dialogs/ProgressDialog.cpp ui/dialogs/ProgressDialog.h + ui/dialogs/ReviewMessageBox.cpp + ui/dialogs/ReviewMessageBox.h ui/dialogs/UpdateDialog.cpp ui/dialogs/UpdateDialog.h ui/dialogs/VersionSelectDialog.cpp @@ -905,6 +907,7 @@ qt5_wrap_ui(LAUNCHER_UI ui/dialogs/AboutDialog.ui ui/dialogs/LoginDialog.ui ui/dialogs/EditAccountDialog.ui + ui/dialogs/ReviewMessageBox.ui ) qt5_add_resources(LAUNCHER_RESOURCES diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 23ca8731..a53f93e8 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -5,7 +5,7 @@ #include #include "ProgressDialog.h" -#include "CustomMessageBox.h" +#include "ReviewMessageBox.h" #include #include @@ -75,27 +75,16 @@ void ModDownloadDialog::confirm() auto keys = modTask.keys(); keys.sort(Qt::CaseInsensitive); - auto info = QString(tr("You're about to download the following mods:")); - info.append("\n\n"); - for(auto task : keys){ - info.append(task); - info.append("\n --> "); - info.append(tr("File name: ")); - info.append(modTask.find(task).value()->getFilename()); - info.append('\n'); - } - - auto confirm_dialog = CustomMessageBox::selectable( + auto confirm_dialog = ReviewMessageBox::create( this, - tr("Confirm mods to download"), - info, - QMessageBox::NoIcon, - QMessageBox::Cancel | QMessageBox::Ok, - QMessageBox::Ok + tr("Confirm mods to download") ); - auto AcceptButton = confirm_dialog->button(QMessageBox::Ok); - connect(AcceptButton, &QPushButton::clicked, this, &ModDownloadDialog::accept); + for(auto task : keys){ + confirm_dialog->appendMod(task, modTask.find(task).value()->getFilename()); + } + + connect(confirm_dialog, &QDialog::accepted, this, &ModDownloadDialog::accept); confirm_dialog->open(); } diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp new file mode 100644 index 00000000..139d05f1 --- /dev/null +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -0,0 +1,31 @@ +#include "ReviewMessageBox.h" +#include "ui_ReviewMessageBox.h" + +ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QString const& icon) + : QDialog(parent), ui(new Ui::ReviewMessageBox) +{ + ui->setupUi(this); +} + +ReviewMessageBox::~ReviewMessageBox() +{ + delete ui; +} + +auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon) -> ReviewMessageBox* +{ + return new ReviewMessageBox(parent, title, icon); +} + +void ReviewMessageBox::appendMod(const QString& name, const QString& filename) +{ + auto itemTop = new QTreeWidgetItem(ui->modTreeWidget); + itemTop->setText(0, name); + + auto filenameItem = new QTreeWidgetItem(itemTop); + filenameItem->setText(0, QString("Filename: %1").arg(filename)); + + itemTop->insertChildren(0, { filenameItem }); + + ui->modTreeWidget->addTopLevelItem(itemTop); +} diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h new file mode 100644 index 00000000..48742cd9 --- /dev/null +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace Ui { +class ReviewMessageBox; +} + +class ReviewMessageBox final : public QDialog { + Q_OBJECT + + public: + static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*; + + void appendMod(const QString& name, const QString& filename); + + ~ReviewMessageBox(); + + private: + ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon); + + Ui::ReviewMessageBox* ui; +}; diff --git a/launcher/ui/dialogs/ReviewMessageBox.ui b/launcher/ui/dialogs/ReviewMessageBox.ui new file mode 100644 index 00000000..d04f3b3f --- /dev/null +++ b/launcher/ui/dialogs/ReviewMessageBox.ui @@ -0,0 +1,98 @@ + + + ReviewMessageBox + + + + 0 + 0 + 400 + 300 + + + + Confirm mod selection + + + true + + + true + + + + + + You're about to download the following mods: + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + true + + + QAbstractItemView::NoSelection + + + QAbstractItemView::SelectItems + + + false + + + + + + + + + + + + + + buttonBox + accepted() + ReviewMessageBox + accept() + + + 200 + 265 + + + 199 + 149 + + + + + buttonBox + rejected() + ReviewMessageBox + reject() + + + 200 + 265 + + + 199 + 149 + + + + + From f1e44291cc794fa16e4c975beb95b1f78584f4e5 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 13 Mar 2022 13:11:35 -0300 Subject: [PATCH 083/605] add translation string --- launcher/ui/dialogs/ReviewMessageBox.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index 139d05f1..2bfd02e0 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -23,7 +23,7 @@ void ReviewMessageBox::appendMod(const QString& name, const QString& filename) itemTop->setText(0, name); auto filenameItem = new QTreeWidgetItem(itemTop); - filenameItem->setText(0, QString("Filename: %1").arg(filename)); + filenameItem->setText(0, tr("Filename: %1").arg(filename)); itemTop->insertChildren(0, { filenameItem }); From ec9d0e70fbd8bf6ecd72a8cf756e46706725320f Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sun, 13 Mar 2022 17:57:25 +0100 Subject: [PATCH 084/605] [macos] update copyright and info string --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5250d0ff..5b1b97d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -170,13 +170,13 @@ if(UNIX AND APPLE) # Mac bundle settings set(MACOSX_BUNDLE_BUNDLE_NAME "${Launcher_Name}") - set(MACOSX_BUNDLE_INFO_STRING "${Launcher_Name}: Minecraft launcher and management utility.") + set(MACOSX_BUNDLE_INFO_STRING "${Launcher_Name}: A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.") set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.polymc.${Launcher_Name}") set(MACOSX_BUNDLE_BUNDLE_VERSION "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}") set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}") set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}") set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns) - set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2015-2021 ${Launcher_Copyright}") + set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2021-2022 ${Launcher_Copyright}") # directories to look for dependencies set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) From a0f76cba62e8e702d8f17d5b7556bd1292cff655 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 14 Mar 2022 18:42:41 +0100 Subject: [PATCH 085/605] chore: clarify GPL-3.0-only --- COPYING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/COPYING.md b/COPYING.md index 1ac6d5cb..273a5b3a 100644 --- a/COPYING.md +++ b/COPYING.md @@ -5,8 +5,7 @@ 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, either version 3 of the License, or - (at your option) any later version. + 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 From 199740cc6101e634923b215ae87d5b60e98235b3 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 14 Mar 2022 18:44:02 +0100 Subject: [PATCH 086/605] fix(metainfo): clarify GPL-3.0-only --- program_info/org.polymc.PolyMC.metainfo.xml.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program_info/org.polymc.PolyMC.metainfo.xml.in b/program_info/org.polymc.PolyMC.metainfo.xml.in index 7972965c..ff4af1c3 100644 --- a/program_info/org.polymc.PolyMC.metainfo.xml.in +++ b/program_info/org.polymc.PolyMC.metainfo.xml.in @@ -9,7 +9,7 @@ PolyMC Team A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once CC0-1.0 - GPL-3.0-or-later + GPL-3.0-only https://polymc.org/ https://polymc.org/wiki/ From 8409aa2571d57f015a634a220107d199e88ba2fd Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 8 Mar 2022 11:12:35 -0300 Subject: [PATCH 087/605] tidy: Fix clang-tidy issues on files changed in this PR The checks used are roughly the same as the ones proposed in the clang-tidy PR (except perhaps that I used modernize-* instead of listing them individually,though I don't think this caused any readability detriments). In ModrinthModel.cpp and FlameModModel.cpp I ignored the modernize-avoid-c-arrays one, mostly because making the sorts array an std::array would most likely increase the code complexity because of the virtual function. Aside from that, the static_cast warning from Application.h was not dealt with, since it's not in this PR's scope. --- launcher/modplatform/flame/FlameAPI.h | 4 +-- launcher/modplatform/flame/FlameModIndex.cpp | 4 +-- launcher/modplatform/flame/FlameModIndex.h | 2 +- .../modplatform/helpers/NetworkModAPI.cpp | 6 ++--- launcher/modplatform/helpers/NetworkModAPI.h | 4 +-- launcher/modplatform/modrinth/ModrinthAPI.h | 10 +++---- .../modrinth/ModrinthPackIndex.cpp | 4 +-- .../modplatform/modrinth/ModrinthPackIndex.h | 2 +- launcher/ui/pages/modplatform/ModModel.cpp | 12 ++++----- launcher/ui/pages/modplatform/ModModel.h | 22 +++++++-------- launcher/ui/pages/modplatform/ModPage.cpp | 8 +++--- launcher/ui/pages/modplatform/ModPage.h | 27 +++++++++---------- .../pages/modplatform/flame/FlameModModel.cpp | 3 ++- .../pages/modplatform/flame/FlameModModel.h | 8 +++--- .../pages/modplatform/flame/FlameModPage.cpp | 4 +-- .../ui/pages/modplatform/flame/FlameModPage.h | 18 ++++++------- .../modplatform/modrinth/ModrinthModel.cpp | 3 ++- .../modplatform/modrinth/ModrinthModel.h | 7 ++--- .../modplatform/modrinth/ModrinthPage.cpp | 4 +-- .../pages/modplatform/modrinth/ModrinthPage.h | 18 ++++++------- 20 files changed, 86 insertions(+), 84 deletions(-) diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index f7f993d7..62accfa4 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -4,7 +4,7 @@ class FlameAPI : public NetworkModAPI { private: - inline QString getModSearchURL(SearchArgs& args) const + inline auto getModSearchURL(SearchArgs& args) const -> QString override { return QString( "https://addons-ecs.forgesvc.net/api/v2/addon/search?" @@ -25,7 +25,7 @@ class FlameAPI : public NetworkModAPI { .arg(args.version); }; - inline QString getVersionsURL(const QString& addonId) const + inline auto getVersionsURL(const QString& addonId) const -> QString override { return QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId); }; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 61cb534c..2c3adee4 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -43,8 +43,8 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, BaseInstance* inst) { QVector unsortedVersions; - bool hasFabric = !((MinecraftInstance*)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); - QString mcVersion = ((MinecraftInstance*)inst)->getPackProfile()->getComponentVersion("net.minecraft"); + bool hasFabric = !(dynamic_cast(inst))->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); + QString mcVersion = (dynamic_cast(inst))->getPackProfile()->getComponentVersion("net.minecraft"); for (auto versionIter : arr) { auto obj = versionIter.toObject(); diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index 34f71498..d3171d94 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -6,8 +6,8 @@ #include "modplatform/ModIndex.h" -#include #include "BaseInstance.h" +#include namespace FlameMod { diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp index ef084535..25c7b9fd 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.cpp +++ b/launcher/modplatform/helpers/NetworkModAPI.cpp @@ -7,7 +7,7 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const { - auto netJob = new NetJob(QString("Modrinth::Search"), APPLICATION->network()); + auto netJob = new NetJob(QString("%1::Search").arg(caller->debugName()), APPLICATION->network()); auto searchUrl = getModSearchURL(args); auto response = new QByteArray(); @@ -16,7 +16,7 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const QObject::connect(netJob, &NetJob::started, caller, [caller, netJob] { caller->setActiveJob(netJob); }); QObject::connect(netJob, &NetJob::failed, caller, &CallerType::searchRequestFailed); QObject::connect(netJob, &NetJob::succeeded, caller, [caller, response] { - QJsonParseError parse_error; + QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset @@ -39,7 +39,7 @@ void NetworkModAPI::getVersions(CallerType* caller, const QString& addonId) cons netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(addonId), response)); QObject::connect(netJob, &NetJob::succeeded, caller, [response, caller, addonId] { - QJsonParseError parse_error; + QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h index 65192046..4d3f7005 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.h +++ b/launcher/modplatform/helpers/NetworkModAPI.h @@ -8,6 +8,6 @@ class NetworkModAPI : public ModAPI { void getVersions(CallerType* caller, const QString& addonId) const override; protected: - virtual QString getModSearchURL(SearchArgs& args) const = 0; - virtual QString getVersionsURL(const QString& addonId) const = 0; + virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0; + virtual auto getVersionsURL(const QString& addonId) const -> QString = 0; }; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 1cc51ff2..cf4dec1a 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -6,10 +6,10 @@ class ModrinthAPI : public NetworkModAPI { public: - inline QString getAuthorURL(const QString& name) const { return "https://modrinth.com/user/" + name; }; + inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; private: - inline QString getModSearchURL(SearchArgs& args) const override + inline auto getModSearchURL(SearchArgs& args) const -> QString override { if (!validateModLoader(args.mod_loader)) { qWarning() << "Modrinth only have Forge and Fabric-compatible mods!"; @@ -30,12 +30,12 @@ class ModrinthAPI : public NetworkModAPI { .arg(args.version); }; - inline QString getVersionsURL(const QString& addonId) const override + inline auto getVersionsURL(const QString& addonId) const -> QString override { return QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId); }; - inline QString getModLoaderString(ModLoaderType modLoader) const + inline auto getModLoaderString(ModLoaderType modLoader) const -> QString { switch (modLoader) { case Any: @@ -49,7 +49,7 @@ class ModrinthAPI : public NetworkModAPI { } } - inline bool validateModLoader(ModLoaderType modLoader) const + inline auto validateModLoader(ModLoaderType modLoader) const -> bool { return modLoader == Any || modLoader == Forge || modLoader == Fabric; } diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 02aac34d..ab6b451b 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -30,8 +30,8 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, BaseInstance* inst) { QVector unsortedVersions; - bool hasFabric = !((MinecraftInstance*)inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); - QString mcVersion = ((MinecraftInstance*)inst)->getPackProfile()->getComponentVersion("net.minecraft"); + bool hasFabric = !(static_cast(inst))->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); + QString mcVersion = (static_cast(inst))->getPackProfile()->getComponentVersion("net.minecraft"); for (auto versionIter : arr) { auto obj = versionIter.toObject(); diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index abfdabb6..fd17847a 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -2,8 +2,8 @@ #include "modplatform/ModIndex.h" -#include #include "BaseInstance.h" +#include namespace Modrinth { diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 6f3c6ce0..cc3c5326 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -11,7 +11,7 @@ namespace ModPlatform { ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) {} -QString ListModel::debugName() const +auto ListModel::debugName() const -> QString { return m_parent->debugName(); } @@ -29,7 +29,7 @@ void ListModel::fetchMore(const QModelIndex& parent) performPaginatedSearch(); } -QVariant ListModel::data(const QModelIndex& index, int role) const +auto ListModel::data(const QModelIndex& index, int role) const -> QVariant { int pos = index.row(); if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } @@ -56,7 +56,7 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return v; } - return QVariant(); + return {}; } void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) @@ -66,8 +66,8 @@ void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) void ListModel::performPaginatedSearch() { - QString mcVersion = ((MinecraftInstance*)((ModPage*)parent())->m_instance)->getPackProfile()->getComponentVersion("net.minecraft"); - bool hasFabric = !((MinecraftInstance*)((ModPage*)parent())->m_instance) + QString mcVersion = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile()->getComponentVersion("net.minecraft"); + bool hasFabric = !(dynamic_cast((dynamic_cast(parent()))->m_instance)) ->getPackProfile() ->getComponentVersion("net.fabricmc.fabric-loader") .isEmpty(); @@ -188,7 +188,7 @@ void ListModel::searchRequestFailed(QString reason) QMessageBox::critical(nullptr, tr("Error"), QString("%1 %2").arg(m_parent->displayName()).arg(tr("API version too old!\nPlease update PolyMC!"))); // self-destruct - ((ModDownloadDialog*)((ModPage*)parent())->parentWidget())->reject(); + (dynamic_cast((dynamic_cast(parent()))->parentWidget()))->reject(); } jobPtr.reset(); diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 6c3ecc3d..02be6049 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -10,24 +10,24 @@ class ModPage; namespace ModPlatform { -typedef QMap LogoMap; -typedef std::function LogoCallback; +using LogoMap = QMap; +using LogoCallback = std::function; class ListModel : public QAbstractListModel { Q_OBJECT public: ListModel(ModPage* parent); - virtual ~ListModel() = default; + ~ListModel() override = default; - inline int rowCount(const QModelIndex& parent) const override { return modpacks.size(); }; - inline int columnCount(const QModelIndex& parent) const override { return 1; }; - inline Qt::ItemFlags flags(const QModelIndex& index) const override { return QAbstractListModel::flags(index); }; + inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); }; + inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; }; + inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; - QString debugName() const; + auto debugName() const -> QString; /* Retrieve information from the model at a given index with the given role */ - QVariant data(const QModelIndex& index, int role) const override; + auto data(const QModelIndex& index, int role) const -> QVariant override; inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } @@ -41,7 +41,7 @@ class ListModel : public QAbstractListModel { void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); - inline bool canFetchMore(const QModelIndex& parent) const override { return searchState == CanPossiblyFetchMore; }; + inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return searchState == CanPossiblyFetchMore; }; public slots: void searchRequestFinished(QJsonDocument& doc); @@ -57,8 +57,8 @@ class ListModel : public QAbstractListModel { void performPaginatedSearch(); protected: - virtual QJsonArray documentToArray(QJsonDocument& obj) const = 0; - virtual const char** getSorts() const = 0; + virtual auto documentToArray(QJsonDocument& obj) const -> QJsonArray = 0; + virtual auto getSorts() const -> const char** = 0; void requestLogo(QString file, QString url); diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 94cf4bcf..1386ba24 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -33,10 +33,10 @@ void ModPage::openedImpl() triggerSearch(); } -bool ModPage::eventFilter(QObject* watched, QEvent* event) +auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool { if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { - QKeyEvent* keyEvent = static_cast(event); + auto* keyEvent = dynamic_cast(event); if (keyEvent->key() == Qt::Key_Return) { triggerSearch(); keyEvent->accept(); @@ -70,7 +70,7 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) text = "" + name + ""; if (!current.authors.empty()) { - auto authorToStr = [](ModPlatform::ModpackAuthor& author) { + auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { if (author.url.isEmpty()) { return author.name; } return QString("%2").arg(author.url, author.name); }; @@ -128,7 +128,7 @@ void ModPage::onModSelected() void ModPage::updateModVersions() { - auto packProfile = (static_cast(m_instance))->getPackProfile(); + auto packProfile = (dynamic_cast(m_instance))->getPackProfile(); QString mcVersion = packProfile->getComponentVersion("net.minecraft"); QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge"; diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index de66b3b0..58024260 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -14,36 +14,35 @@ namespace Ui { class ModPage; } -/* This page handles most logic related to browsing and selecting mods to download. - * By default, the methods provided work with net requests, to fetch data from remote APIs. */ +/* This page handles most logic related to browsing and selecting mods to download. */ class ModPage : public QWidget, public BasePage { Q_OBJECT public: explicit ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api); - virtual ~ModPage(); + ~ModPage() override; /* Affects what the user sees */ - virtual QString displayName() const override = 0; - virtual QIcon icon() const override = 0; - virtual QString id() const override = 0; - virtual QString helpPage() 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; /* Used internally */ - virtual QString metaEntryBase() const = 0; - virtual QString debugName() const = 0; + virtual auto metaEntryBase() const -> QString = 0; + virtual auto debugName() const -> QString = 0; - virtual bool shouldDisplay() const override = 0; - virtual bool validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const = 0; + auto shouldDisplay() const -> bool override = 0; + virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool = 0; - const ModAPI* apiProvider() const { return api.get(); }; + auto apiProvider() const -> const ModAPI* { return api.get(); }; - ModPlatform::IndexedPack& getCurrent() { return current; } + auto getCurrent() -> ModPlatform::IndexedPack& { return current; } void updateModVersions(); void openedImpl() override; - bool eventFilter(QObject* watched, QEvent* event) override; + auto eventFilter(QObject* watched, QEvent* event) -> bool override; BaseInstance* m_instance; diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index 8437f623..905fb2dd 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -4,6 +4,7 @@ namespace FlameMod { +// NOLINTNEXTLINE(modernize-avoid-c-arrays) const char* ListModel::sorts[6]{ "Featured", "Popularity", "LastUpdated", "Name", "Author", "TotalDownloads" }; void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) @@ -16,7 +17,7 @@ void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); } -QJsonArray ListModel::documentToArray(QJsonDocument& obj) const +auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { return obj.array(); } diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h index cf3042ed..707c1bb1 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h @@ -9,17 +9,17 @@ class ListModel : public ModPlatform::ListModel { public: ListModel(FlameModPage* parent) : ModPlatform::ListModel(parent) {} -; - virtual ~ListModel() = default; + ~ListModel() override = default; private: void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - QJsonArray documentToArray(QJsonDocument& obj) const override; + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; + // NOLINTNEXTLINE(modernize-avoid-c-arrays) static const char* sorts[6]; - inline const char** getSorts() const override { return sorts; }; + inline auto getSorts() const -> const char** override { return sorts; }; }; } // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 091e49c7..c409a5cb 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -26,7 +26,7 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected); } -bool FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const +auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool { (void) loaderVer; return ver.mcVersion.contains(mineVer); @@ -35,4 +35,4 @@ bool FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString min // 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... -bool FlameModPage::shouldDisplay() const { return true; } +auto FlameModPage::shouldDisplay() const -> bool { return true; } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 90513ca5..b48216bb 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -9,17 +9,17 @@ class FlameModPage : public ModPage { public: explicit FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance); - virtual ~FlameModPage() = default; + ~FlameModPage() override = default; - inline QString displayName() const override { return tr("CurseForge"); } - inline QIcon icon() const override { return APPLICATION->getThemedIcon("flame"); } - inline QString id() const override { return "curseforge"; } - inline QString helpPage() const override { return "Flame-platform"; } + inline auto displayName() const -> QString override { return tr("CurseForge"); } + inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("flame"); } + inline auto id() const -> QString override { return "curseforge"; } + inline auto helpPage() const -> QString override { return "Flame-platform"; } - inline QString debugName() const override { return tr("Flame"); } - inline QString metaEntryBase() const override { return "FlameMods"; }; + inline auto debugName() const -> QString override { return tr("Flame"); } + inline auto metaEntryBase() const -> QString override { return "FlameMods"; }; - bool validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const override; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override; - bool shouldDisplay() const override; + auto shouldDisplay() const -> bool override; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index ac3c14f2..daa43e26 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -4,6 +4,7 @@ namespace Modrinth { +// NOLINTNEXTLINE(modernize-avoid-c-arrays) const char* ListModel::sorts[5]{ "relevance", "downloads", "follows", "updated", "newest" }; void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) @@ -16,7 +17,7 @@ void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); } -QJsonArray ListModel::documentToArray(QJsonDocument& obj) const +auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { return obj.object().value("hits").toArray(); } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index de73704c..45a6090a 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -9,16 +9,17 @@ class ListModel : public ModPlatform::ListModel { public: ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent){}; - virtual ~ListModel() = default; + ~ListModel() override = default; private: void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - QJsonArray documentToArray(QJsonDocument& obj) const override; + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; + // NOLINTNEXTLINE(modernize-avoid-c-arrays) static const char* sorts[5]; - inline const char** getSorts() const override { return sorts; }; + inline auto getSorts() const -> const char** override { return sorts; }; }; } // namespace Modrinth diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index cf01506e..aebfee74 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -25,7 +25,7 @@ ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthPage::onModSelected); } -bool ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const +auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool { return ver.mcVersion.contains(mineVer) && ver.loaders.contains(loaderVer); } @@ -33,4 +33,4 @@ bool ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString min // 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... -bool ModrinthPage::shouldDisplay() const { return true; } +auto ModrinthPage::shouldDisplay() const -> bool { return true; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 6f387708..6e911b83 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -9,17 +9,17 @@ class ModrinthPage : public ModPage { public: explicit ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance); - virtual ~ModrinthPage() = default; + ~ModrinthPage() override = default; - inline QString displayName() const override { return tr("Modrinth"); } - inline QIcon icon() const override { return APPLICATION->getThemedIcon("modrinth"); } - inline QString id() const override { return "modrinth"; } - inline QString helpPage() const override { return "Modrinth-platform"; } + inline auto displayName() const -> QString override { return tr("Modrinth"); } + inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); } + inline auto id() const -> QString override { return "modrinth"; } + inline auto helpPage() const -> QString override { return "Modrinth-platform"; } - inline QString debugName() const override { return tr("Modrinth"); } - inline QString metaEntryBase() const override { return "ModrinthPacks"; }; + inline auto debugName() const -> QString override { return tr("Modrinth"); } + inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; }; - bool validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const override; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override; - bool shouldDisplay() const override; + auto shouldDisplay() const -> bool override; }; From a268ac71410c228d0d76cd2a9cf9a6057b1f9085 Mon Sep 17 00:00:00 2001 From: Philipp David Date: Tue, 15 Mar 2022 09:04:43 +0100 Subject: [PATCH 088/605] Add GITDIR-NOTFOUND check This adds a check for a GIT_REFSPEC value of "GITDIR-NOTFOUND" and sets the VERSION_CHANNEL to stable in that case. Without this change, "GITDIR-N" is appended to the version string when building from a source archive instead of a git checkout. --- buildconfig/BuildConfig.cpp.in | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 6524fb5d..4625b1bf 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -28,7 +28,11 @@ Config::Config() GIT_COMMIT = "@Launcher_GIT_COMMIT@"; GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; - if(GIT_REFSPEC.startsWith("refs/heads/")) + if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND")) + { + VERSION_CHANNEL = QStringLiteral("stable"); + } + else if(GIT_REFSPEC.startsWith("refs/heads/")) { VERSION_CHANNEL = GIT_REFSPEC; VERSION_CHANNEL.remove("refs/heads/"); From 440e9731e2c49613891785436101423d3b6bc883 Mon Sep 17 00:00:00 2001 From: Philipp David Date: Thu, 17 Mar 2022 16:55:45 +0100 Subject: [PATCH 089/605] Switch to msys2 for Windows builds --- .github/workflows/build.yml | 95 ++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b5797e95..a0736f76 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,9 +25,12 @@ jobs: app_image: true - os: windows-2022 - qt_version: 5.15.2 - qt_host: windows - qt_arch: win32_mingw81 + name: "Windows-i686" + msystem: mingw32 + + - os: windows-2022 + name: "Windows-x86_64" + msystem: mingw64 - os: macos-11 qt_version: 5.12.12 @@ -42,38 +45,24 @@ jobs: BUILD_DIR: "build" steps: - - name: Install 32bit mingw on Windows - if: runner.os == 'Windows' - uses: egor-tensin/setup-mingw@v2 - with: - platform: x86 - - - name: Install 32bit zlib via Strawberry on Windows - if: runner.os == 'Windows' - run: | - choco install strawberryperl -y --force --x86 - - name: Checkout uses: actions/checkout@v2 with: submodules: 'true' - # We need to do this here because it inexplicably fails if we split the step - - name: Download and install OpenSSL libs on Windows + - name: 'Setup MSYS2' if: runner.os == 'Windows' - run: | - python -m pip install --upgrade pip - python -m pip install aqtinstall==2.0.5 - python -m aqt install-tool -O "${{ github.workspace }}\Qt\" windows desktop tools_openssl_x86 - mkdir ${{ env.INSTALL_DIR }} - copy "${{ github.workspace }}\Qt\Tools\OpenSSL\Win_x86\bin\libssl-1_1.dll" "${{ github.workspace }}\${{ env.INSTALL_DIR }}\" - copy "${{ github.workspace }}\Qt\Tools\OpenSSL\Win_x86\bin\libcrypto-1_1.dll" "${{ github.workspace }}\${{ env.INSTALL_DIR }}\" - - - name: Set short version - shell: bash - run: | - ver_short=`git rev-parse --short HEAD` - echo "VERSION=$ver_short" >> $GITHUB_ENV + uses: msys2/setup-msys2@v2 + with: + msystem: ${{ matrix.msystem }} + update: true + install: >- + git + pacboy: >- + toolchain:p + cmake:p + ninja:p + qt5:p - name: Install OpenJDK uses: AdoptOpenJDK/install-jdk@v1 @@ -81,6 +70,7 @@ jobs: version: '17' - name: Cache Qt + if: runner.os != 'Windows' id: cache-qt uses: actions/cache@v2 with: @@ -88,7 +78,7 @@ jobs: key: ${{ runner.os }}-${{ matrix.qt_version }}-${{ matrix.qt_arch }}-qt_cache - name: Install Qt - if: runner.os != 'Linux' || matrix.app_image == true + if: runner.os != 'Linux' && runner.os != 'Windows' || matrix.app_image == true uses: jurplel/install-qt-action@v2 with: version: ${{ matrix.qt_version }} @@ -104,6 +94,7 @@ jobs: sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 - name: Install Ninja + if: runner.os != 'Windows' uses: urkle/action-get-ninja@v1 - name: Download linuxdeploy family for AppImage on Linux @@ -120,7 +111,13 @@ jobs: ${{ github.workspace }}/.github/scripts/prepare_JREs.sh - name: Configure CMake - if: runner.os != 'Linux' + if: runner.os != 'Linux' && runner.os != 'Windows' + run: | + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja + + - name: Configure CMake on Windows + if: runner.os == 'Windows' + shell: msys2 {0} run: | cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja @@ -130,11 +127,24 @@ jobs: cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_PORTABLE=OFF -G Ninja - name: Build + if: runner.os != 'Windows' + run: | + cmake --build ${{ env.BUILD_DIR }} + + - name: Build on Windows + if: runner.os == 'Windows' + shell: msys2 {0} run: | cmake --build ${{ env.BUILD_DIR }} - name: Install - if: runner.os != 'Linux' + if: runner.os != 'Linux' && runner.os != 'Windows' + run: | + cmake --install ${{ env.BUILD_DIR }} + + - name: Install on Windows + if: runner.os == 'Windows' + shell: msys2 {0} run: | cmake --install ${{ env.BUILD_DIR }} @@ -165,11 +175,6 @@ jobs: ./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_DIR }}/usr/share/icons/hicolor/scalable/apps/org.polymc.PolyMC.svg - - name: Run windeployqt - if: runner.os == 'Windows' - run: | - windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw "${{ env.INSTALL_DIR }}/polymc.exe" - - name: Run macdeployqt if: runner.os == 'macOS' run: | @@ -207,11 +212,25 @@ jobs: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage path: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage + - name: Copy OpenSSL libs on Windows x86 + if: runner.os == 'Windows' && matrix.msystem == 'mingw32' + shell: msys2 {0} + run: | + cp /mingw32/bin/libcrypto-1_1.dll ${{ env.INSTALL_DIR }}/ + cp /mingw32/bin/libssl-1_1.dll ${{ env.INSTALL_DIR }}/ + + - name: Copy OpenSSL libs on Windows x86_64 + if: runner.os == 'Windows' && matrix.msystem == 'mingw64' + shell: msys2 {0} + run: | + cp /mingw64/bin/libcrypto-1_1-x64.dll ${{ env.INSTALL_DIR }}/ + cp /mingw64/bin/libssl-1_1-x64.dll ${{ env.INSTALL_DIR }}/ + - name: Upload package for Windows if: runner.os == 'Windows' uses: actions/upload-artifact@v2 with: - name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} + name: PolyMC-${{ matrix.name }}-${{ inputs.build_type }} path: ${{ env.INSTALL_DIR }}/** - name: Upload package for macOS From f01b8f29c68cde62d31293175482d1d0c9bc23e0 Mon Sep 17 00:00:00 2001 From: Philipp David Date: Thu, 17 Mar 2022 23:32:44 +0100 Subject: [PATCH 090/605] Use Temurin instead of AdoptOpenJDK --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a0736f76..325e28b8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,9 +65,10 @@ jobs: qt5:p - name: Install OpenJDK - uses: AdoptOpenJDK/install-jdk@v1 + uses: actions/setup-java@v3 with: - version: '17' + distribution: 'temurin' + java-version: '17' - name: Cache Qt if: runner.os != 'Windows' From 2d1f99b765ef0870c2a78db890c673bb6d0bbcae Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 18 Mar 2022 11:38:13 +0100 Subject: [PATCH 091/605] fix: make Launcher_PORTABLE work on all platforms Fixes #261 --- CMakeLists.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7cd0cf86..7537703c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -152,6 +152,10 @@ add_subdirectory(program_info) # Install the build results according to platform set(Launcher_PORTABLE 1 CACHE BOOL "The type of installation (Portable or System)") +if (Launcher_PORTABLE) + # launcher/Application.cpp will use this value + set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_PORTABLE") +endif() if(UNIX AND APPLE) set(BINARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") @@ -191,9 +195,6 @@ elseif(UNIX) set(BUNDLE_DEST_DIR ".") set(JARS_DEST_DIR "bin/jars") - # launcher/Application.cpp will use this value - set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_PORTABLE") - # Install basic runner script configure_file(launcher/Launcher.in "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" @ONLY) install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" DESTINATION ${BUNDLE_DEST_DIR} RENAME ${Launcher_Name}) From fa5fa53592952a4108696db625fd1fe227faa11b Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 18 Mar 2022 10:52:47 -0300 Subject: [PATCH 092/605] fix: Use primary file for mod download on Modrinth --- .../modrinth/ModrinthPackIndex.cpp | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 9017eb67..9581ca04 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -51,31 +51,32 @@ void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray auto files = Json::requireArray(obj, "files"); int i = 0; - while (files.count() > 1 && i < files.count()){ - //try to resolve the correct file + + // Find correct file (needed in cases where one version may have multiple files) + // Will default to the last one if there's no primary (though I think Modrinth requires that + // at least one file is primary, idk) + while (i < files.count()){ auto parent = files[i].toObject(); auto fileName = Json::requireString(parent, "filename"); - //avoid grabbing "dev" files - if(fileName.contains("javadocs",Qt::CaseInsensitive) || fileName.contains("sources",Qt::CaseInsensitive)){ + + // Grab the correct mod loader + if(hasFabric){ + if(fileName.contains("forge",Qt::CaseInsensitive)){ + i++; + continue; + } + } else if(fileName.contains("fabric", Qt::CaseInsensitive)){ i++; continue; } - //grab the correct mod loader - if(fileName.contains("forge",Qt::CaseInsensitive) || fileName.contains("fabric",Qt::CaseInsensitive) ){ - if(hasFabric){ - if(fileName.contains("forge",Qt::CaseInsensitive)){ - i++; - continue; - } - }else{ - if(fileName.contains("fabric",Qt::CaseInsensitive)){ - i++; - continue; - } - } - } - break; + + // Grab the primary file, if available + if(Json::requireBoolean(parent, "primary")) + break; + + i++; } + auto parent = files[i].toObject(); if(parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); From 19804c571878196e034b0366b61aa0ab9f577a3b Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Fri, 18 Mar 2022 15:28:44 +0100 Subject: [PATCH 093/605] bundle jre8u312 instead of latest 8u320 or higher breaks old forge --- .github/scripts/prepare_JREs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/prepare_JREs.sh b/.github/scripts/prepare_JREs.sh index b85e9c2f..ee713f81 100755 --- a/.github/scripts/prepare_JREs.sh +++ b/.github/scripts/prepare_JREs.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -URL_JDK8="https://api.adoptium.net/v3/binary/latest/8/ga/linux/x64/jre/hotspot/normal/eclipse" +URL_JDK8="https://api.adoptium.net/v3/binary/version/jdk8u312-b07/linux/x64/jre/hotspot/normal/eclipse" URL_JDK17="https://api.adoptium.net/v3/binary/latest/17/ga/linux/x64/jre/hotspot/normal/eclipse" mkdir -p JREs From ec66c8fd3d7cb785d701f258d606b7a46be45a8b Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 18 Mar 2022 14:21:42 -0300 Subject: [PATCH 094/605] fix(ui): remove paste.polymc.org --- launcher/ui/pages/global/APIPage.ui | 5 ----- 1 file changed, 5 deletions(-) diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 28c53b79..1bc41e5a 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -74,11 +74,6 @@ https://0x0.st
- - - https://paste.polymc.org - -
From 620252537295fa7dd5ce8a27ce03561a597ed0cd Mon Sep 17 00:00:00 2001 From: Philipp David Date: Fri, 18 Mar 2022 22:28:52 +0100 Subject: [PATCH 095/605] Readd short rev to artifact names --- .github/workflows/build.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 325e28b8..ac181079 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,6 +64,12 @@ jobs: ninja:p qt5:p + - name: Set short version + shell: bash + run: | + ver_short=`git rev-parse --short HEAD` + echo "VERSION=$ver_short" >> $GITHUB_ENV + - name: Install OpenJDK uses: actions/setup-java@v3 with: @@ -231,7 +237,7 @@ jobs: if: runner.os == 'Windows' uses: actions/upload-artifact@v2 with: - name: PolyMC-${{ matrix.name }}-${{ inputs.build_type }} + name: PolyMC-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} path: ${{ env.INSTALL_DIR }}/** - name: Upload package for macOS From 48c2146a420d8474359cce5b507606fdb2a6988f Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 18 Mar 2022 13:19:09 +0100 Subject: [PATCH 096/605] fix(i18n): fix translatable strings --- launcher/InstanceImportTask.cpp | 2 +- launcher/JavaCommon.cpp | 8 ++++---- launcher/minecraft/launch/DirectJavaLaunch.cpp | 2 +- launcher/minecraft/launch/LauncherPartLaunch.cpp | 2 +- launcher/minecraft/update/FoldersTask.cpp | 2 +- launcher/modplatform/legacy_ftb/PackInstallTask.cpp | 2 +- launcher/modplatform/technic/TechnicPackProcessor.cpp | 2 +- launcher/ui/MainWindow.cpp | 6 +++--- launcher/ui/pages/global/APIPage.ui | 2 +- launcher/ui/pages/modplatform/atlauncher/AtlPage.ui | 2 +- launcher/ui/pages/modplatform/flame/FlameModPage.cpp | 4 ++-- launcher/ui/pages/modplatform/flame/FlameModPage.ui | 2 +- launcher/ui/pages/modplatform/flame/FlamePage.ui | 2 +- launcher/ui/pages/modplatform/ftb/FtbPage.ui | 2 +- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 6 +++--- launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui | 2 +- launcher/ui/pages/modplatform/technic/TechnicPage.ui | 2 +- launcher/ui/setupwizard/JavaWizardPage.cpp | 2 +- launcher/ui/widgets/CustomCommands.ui | 2 +- launcher/ui/widgets/JavaSettingsWidget.cpp | 4 ++-- 20 files changed, 29 insertions(+), 29 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 6dd615c7..a825e8d4 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -283,7 +283,7 @@ void InstanceImportTask::processFlame() } else { - logWarning(tr("Could not map recommended forge version for Minecraft %1").arg(mcVersion)); + logWarning(tr("Could not map recommended Forge version for Minecraft %1").arg(mcVersion)); } } components->setComponentVersion("net.minecraftforge", forgeVersion); diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index 6fa5851b..a6542fa7 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -8,7 +8,7 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent) || 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\" or \"-Xms\").\n" + "You tried to manually set a JVM memory option (using \"-XX:PermSize\", \"-XX-MaxHeapSize\", \"-XX:InitialHeapSize\", \"-Xmx\" or \"-Xms\").\n" "There are dedicated boxes for these in the settings (Java tab, in the Memory group at the top).\n" "This message will be displayed until you remove them from the JVM arguments."); CustomMessageBox::selectable( @@ -40,7 +40,7 @@ void JavaCommon::javaArgsWereBad(QWidget *parent, JavaCheckResult result) auto htmlError = result.errorLog; QString text; htmlError.replace('\n', "
"); - text += QObject::tr("The specified java binary didn't work with the arguments you provided:
"); + text += QObject::tr("The specified Java binary didn't work with the arguments you provided:
"); text += QString("%1").arg(htmlError); CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } @@ -49,8 +49,8 @@ void JavaCommon::javaBinaryWasBad(QWidget *parent, JavaCheckResult result) { QString text; text += QObject::tr( - "The specified java binary didn't work.
You should use the auto-detect feature, " - "or set the path to the java executable.
"); + "The specified Java binary didn't work.
You should use the auto-detect feature, " + "or set the path to the Java executable.
"); CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } diff --git a/launcher/minecraft/launch/DirectJavaLaunch.cpp b/launcher/minecraft/launch/DirectJavaLaunch.cpp index 2bcff664..742170fa 100644 --- a/launcher/minecraft/launch/DirectJavaLaunch.cpp +++ b/launcher/minecraft/launch/DirectJavaLaunch.cpp @@ -88,7 +88,7 @@ void DirectJavaLaunch::on_state(LoggedProcess::State state) case LoggedProcess::FailedToStart: { //: Error message displayed if instance can't start - const char *reason = QT_TR_NOOP("Could not launch minecraft!"); + const char *reason = QT_TR_NOOP("Could not launch Minecraft!"); emit logLine(reason, MessageLevel::Fatal); emitFailed(tr(reason)); return; diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index f461b847..d15d7e9d 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -154,7 +154,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) case LoggedProcess::FailedToStart: { //: Error message displayed if instace can't start - const char *reason = QT_TR_NOOP("Could not launch minecraft!"); + const char *reason = QT_TR_NOOP("Could not launch Minecraft!"); emit logLine(reason, MessageLevel::Fatal); emitFailed(tr(reason)); return; diff --git a/launcher/minecraft/update/FoldersTask.cpp b/launcher/minecraft/update/FoldersTask.cpp index e2b1bb48..22768bd9 100644 --- a/launcher/minecraft/update/FoldersTask.cpp +++ b/launcher/minecraft/update/FoldersTask.cpp @@ -14,7 +14,7 @@ void FoldersTask::executeTask() QDir mcDir(m_inst->gameRoot()); if (!mcDir.exists() && !mcDir.mkpath(".")) { - emitFailed(tr("Failed to create folder for minecraft binaries.")); + emitFailed(tr("Failed to create folder for Minecraft binaries.")); return; } emitSucceeded(); diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index f655a066..c63a9f1e 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -114,7 +114,7 @@ void PackInstallTask::install() //ok, found minecraft dir, move contents to instance dir if(!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft")) { - emitFailed(tr("Failed to move unzipped minecraft!")); + emitFailed(tr("Failed to move unzipped Minecraft!")); return; } } diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index 156a295a..782fb9b2 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -88,7 +88,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const else { if (minecraftVersion.isEmpty()) - emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but minecraft version is unknown")); + emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown")); components->setComponentVersion("net.minecraft", minecraftVersion, true); components->installJarMods({modpackJar}); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 7b758e05..60bc8167 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -565,7 +565,7 @@ public: actionViewSelectedMCFolder = TranslatedAction(MainWindow); actionViewSelectedMCFolder->setObjectName(QStringLiteral("actionViewSelectedMCFolder")); actionViewSelectedMCFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Minecraft Folder")); - actionViewSelectedMCFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's minecraft folder in a file browser.")); + actionViewSelectedMCFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's Minecraft folder in a file browser.")); all_actions.append(&actionViewSelectedMCFolder); instanceToolBar->addAction(actionViewSelectedMCFolder); @@ -603,7 +603,7 @@ public: actionDeleteInstance = TranslatedAction(MainWindow); actionDeleteInstance->setObjectName(QStringLiteral("actionDeleteInstance")); - actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Delete")); + actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Delete Instance")); actionDeleteInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Delete the selected instance.")); all_actions.append(&actionDeleteInstance); instanceToolBar->addAction(actionDeleteInstance); @@ -1567,7 +1567,7 @@ void MainWindow::deleteGroup() QString groupName = map["group"].toString(); if(!groupName.isEmpty()) { - auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1") + auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1?") .arg(groupName), QMessageBox::Yes | QMessageBox::No); if(reply == QMessageBox::Yes) { diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 1bc41e5a..7a9088d1 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -71,7 +71,7 @@ - https://0x0.st + https://0x0.st diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui index 9085766a..746aa6d1 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui @@ -71,7 +71,7 @@ - Search and filter ... + Search and filter... true diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 114ac907..1801c5a8 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -153,7 +153,7 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) { ui->versionSelectionBox->addItem(version.version, QVariant(i)); } if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No Valid Version found!"), + ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); } @@ -171,7 +171,7 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) { QVariant(i)); } if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No Valid Version found!"), + ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.ui b/launcher/ui/pages/modplatform/flame/FlameModPage.ui index 36df7e8a..25cb2571 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.ui +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.ui @@ -41,7 +41,7 @@ - Search and filter ... + Search and filter... diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index 9723815a..6d8d8e10 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -71,7 +71,7 @@ - Search and filter ... + Search and filter... diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.ui b/launcher/ui/pages/modplatform/ftb/FtbPage.ui index e9c783e3..850bf091 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.ui +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.ui @@ -34,7 +34,7 @@ - Search and filter ... + Search and filter... true diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 35cd743a..26afe88a 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -27,7 +27,7 @@ ModrinthPage::ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance) ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); // index is used to set the sorting with the modrinth api - ui->sortByBox->addItem(tr("Sort by Relevence")); + ui->sortByBox->addItem(tr("Sort by Relevance")); ui->sortByBox->addItem(tr("Sort by Downloads")); ui->sortByBox->addItem(tr("Sort by Follows")); ui->sortByBox->addItem(tr("Sort by last updated")); @@ -139,7 +139,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { ui->versionSelectionBox->addItem(version.version, QVariant(i)); } if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No Valid Version found !"), + ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); } @@ -159,7 +159,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { QVariant(i)); } if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No Valid Version found !"), + ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index d0a8b8f7..6c709825 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -51,7 +51,7 @@ - Search and filter ... + Search and filter... diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index dde685d9..62ab6154 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -29,7 +29,7 @@ - Search and filter ... + Search and filter... diff --git a/launcher/ui/setupwizard/JavaWizardPage.cpp b/launcher/ui/setupwizard/JavaWizardPage.cpp index 63b3d480..14683778 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.cpp +++ b/launcher/ui/setupwizard/JavaWizardPage.cpp @@ -93,6 +93,6 @@ void JavaWizardPage::retranslate() { setTitle(tr("Java")); setSubTitle(tr("You do not have a working Java set up yet or it went missing.\n" - "Please select one of the following or browse for a java executable.")); + "Please select one of the following or browse for a Java executable.")); m_java_widget->retranslate(); } diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index 21964ad2..dbd54431 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -74,7 +74,7 @@ - <html><head/><body><p>Pre-launch command runs before the instance launches and post-exit command runs after it exits.</p><p>Both will be run in the launcher's working folder with extra environment variables:</p><ul><li>$INST_NAME - Name of the instance</li><li>$INST_ID - ID of the instance (its folder name)</li><li>$INST_DIR - absolute path of the instance</li><li>$INST_MC_DIR - absolute path of minecraft</li><li>$INST_JAVA - java binary used for launch</li><li>$INST_JAVA_ARGS - command-line parameters used for launch</li></ul><p>Wrapper command allows launching using an extra wrapper program (like 'optirun' on Linux)</p></body></html> + <html><head/><body><p>Pre-launch command runs before the instance launches and post-exit command runs after it exits.</p><p>Both will be run in the launcher's working folder with extra environment variables:</p><ul><li>$INST_NAME - Name of the instance</li><li>$INST_ID - ID of the instance (its folder name)</li><li>$INST_DIR - absolute path of the instance</li><li>$INST_MC_DIR - absolute path of Minecraft</li><li>$INST_JAVA - Java binary used for launch</li><li>$INST_JAVA_ARGS - command-line parameters used for launch</li></ul><p>Wrapper command allows launching using an extra wrapper program (like 'optirun' on Linux)</p></body></html> Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index ed07e082..340518b1 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -287,7 +287,7 @@ void JavaSettingsWidget::on_javaStatusBtn_clicked() break; case JavaStatus::DoesNotStart: { - text += QObject::tr("The specified java binary didn't start properly.
"); + text += QObject::tr("The specified Java binary didn't start properly.
"); auto htmlError = m_result.errorLog; if(!htmlError.isEmpty()) { @@ -299,7 +299,7 @@ void JavaSettingsWidget::on_javaStatusBtn_clicked() } case JavaStatus::ReturnedInvalidData: { - text += QObject::tr("The specified java binary returned unexpected results:
"); + text += QObject::tr("The specified Java binary returned unexpected results:
"); auto htmlOut = m_result.outLog; if(!htmlOut.isEmpty()) { From ccfd06ad2141663f38ac42dd8c68ed6253bdfdde Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 19 Mar 2022 12:35:15 +0100 Subject: [PATCH 097/605] fix(i18n): remove brand names from translations --- launcher/ui/pages/modplatform/atlauncher/AtlPage.h | 2 +- launcher/ui/pages/modplatform/flame/FlameModPage.h | 2 +- launcher/ui/pages/modplatform/flame/FlamePage.h | 2 +- launcher/ui/pages/modplatform/ftb/FtbPage.h | 2 +- launcher/ui/pages/modplatform/legacy_ftb/Page.h | 2 +- launcher/ui/pages/modplatform/modrinth/ModrinthPage.h | 2 +- launcher/ui/pages/modplatform/technic/TechnicPage.h | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index 5b3f2228..150edd02 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -42,7 +42,7 @@ public: virtual ~AtlPage(); virtual QString displayName() const override { - return tr("ATLauncher"); + return "ATLauncher"; } virtual QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index b5b19a4f..115ebab2 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -27,7 +27,7 @@ public: virtual ~FlameModPage(); virtual QString displayName() const override { - return tr("CurseForge"); + return "CurseForge"; } virtual QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 5cfe21dc..b0a25a26 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -42,7 +42,7 @@ public: virtual ~FlamePage(); virtual QString displayName() const override { - return tr("CurseForge"); + return "CurseForge"; } virtual QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.h b/launcher/ui/pages/modplatform/ftb/FtbPage.h index 28a189f0..c4a242dd 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.h +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.h @@ -40,7 +40,7 @@ public: virtual ~FtbPage(); virtual QString displayName() const override { - return tr("FTB"); + return "FTB"; } virtual QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index d8225e11..59911908 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -50,7 +50,7 @@ public: virtual ~Page(); QString displayName() const override { - return tr("FTB Legacy"); + return "FTB Legacy"; } QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 52b538e3..402c8770 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -27,7 +27,7 @@ public: virtual ~ModrinthPage(); virtual QString displayName() const override { - return tr("Modrinth"); + return "Modrinth"; } virtual QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index 21695dd0..3635472e 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -42,7 +42,7 @@ public: virtual ~TechnicPage(); virtual QString displayName() const override { - return tr("Technic"); + return "Technic"; } virtual QIcon icon() const override { From 7e0312493bf534ae2b4a09211f7459367dd80495 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 19 Mar 2022 12:36:04 +0100 Subject: [PATCH 098/605] fix(i18n): improve social platform actions --- launcher/ui/MainWindow.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 60bc8167..f45d79d4 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -347,7 +347,7 @@ public: actionMATRIX = TranslatedAction(MainWindow); actionMATRIX->setObjectName(QStringLiteral("actionMATRIX")); actionMATRIX->setIcon(APPLICATION->getThemedIcon("matrix")); - actionMATRIX.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Matrix")); + actionMATRIX.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Matrix space")); actionMATRIX.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Matrix space")); all_actions.append(&actionMATRIX); helpMenu->addAction(actionMATRIX); @@ -357,7 +357,7 @@ public: actionDISCORD = TranslatedAction(MainWindow); actionDISCORD->setObjectName(QStringLiteral("actionDISCORD")); actionDISCORD->setIcon(APPLICATION->getThemedIcon("discord")); - actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Discord")); + actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Discord guild")); actionDISCORD.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Discord guild.")); all_actions.append(&actionDISCORD); helpMenu->addAction(actionDISCORD); @@ -367,7 +367,7 @@ public: actionREDDIT = TranslatedAction(MainWindow); actionREDDIT->setObjectName(QStringLiteral("actionREDDIT")); actionREDDIT->setIcon(APPLICATION->getThemedIcon("reddit-alien")); - actionREDDIT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Reddit")); + actionREDDIT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Subreddit")); actionREDDIT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 subreddit.")); all_actions.append(&actionREDDIT); helpMenu->addAction(actionREDDIT); From a160bd00627d935ca3a330c29c0d80914b6dedb5 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 19 Mar 2022 12:46:56 +0100 Subject: [PATCH 099/605] chore: add license header to files I modified --- buildconfig/BuildConfig.cpp.in | 35 ++++++++++++++++ buildconfig/BuildConfig.h | 35 ++++++++++++++++ launcher/Application.cpp | 35 ++++++++++++++++ launcher/Application.h | 35 ++++++++++++++++ launcher/BaseInstance.cpp | 40 ++++++++++++++----- launcher/BaseInstance.h | 40 ++++++++++++++----- launcher/LaunchController.cpp | 35 ++++++++++++++++ launcher/LaunchController.h | 35 ++++++++++++++++ launcher/MMCZip.cpp | 40 ++++++++++++++----- launcher/MMCZip.h | 40 ++++++++++++++----- launcher/minecraft/auth/AccountData.cpp | 35 ++++++++++++++++ launcher/minecraft/auth/AccountData.h | 35 ++++++++++++++++ launcher/minecraft/auth/AccountList.cpp | 40 ++++++++++++++----- launcher/minecraft/auth/AccountList.h | 40 ++++++++++++++----- launcher/minecraft/auth/AccountTask.cpp | 40 ++++++++++++++----- launcher/minecraft/auth/AccountTask.h | 40 ++++++++++++++----- launcher/minecraft/auth/MinecraftAccount.cpp | 42 +++++++++++++++----- launcher/minecraft/auth/MinecraftAccount.h | 40 ++++++++++++++----- launcher/minecraft/auth/steps/MSAStep.cpp | 35 ++++++++++++++++ launcher/minecraft/auth/steps/MSAStep.h | 34 ++++++++++++++++ launcher/ui/dialogs/ExportInstanceDialog.cpp | 40 ++++++++++++++----- launcher/ui/dialogs/MSALoginDialog.cpp | 40 ++++++++++++++----- launcher/ui/pages/global/APIPage.cpp | 40 ++++++++++++++----- launcher/ui/pages/global/APIPage.h | 40 ++++++++++++++----- launcher/ui/pages/global/AccountListPage.cpp | 40 ++++++++++++++----- launcher/ui/pages/global/AccountListPage.h | 40 ++++++++++++++----- 26 files changed, 830 insertions(+), 161 deletions(-) diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 4625b1bf..7360d964 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "BuildConfig.h" #include diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 79210505..2fb71f14 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 diff --git a/launcher/Application.cpp b/launcher/Application.cpp index e0d7ba0a..33b1774c 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "Application.h" #include "BuildConfig.h" diff --git a/launcher/Application.h b/launcher/Application.h index fb41d647..c3e29ef5 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 1bff9e1d..2fb31d94 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "BaseInstance.h" diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 488f2781..c973fcd4 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 4fd2eade..792d8381 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "LaunchController.h" #include "minecraft/auth/AccountList.h" #include "Application.h" diff --git a/launcher/LaunchController.h b/launcher/LaunchController.h index 7ed4b09e..2171ad5e 100644 --- a/launcher/LaunchController.h +++ b/launcher/LaunchController.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 9d7e4cc2..b92f1781 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index 0f7aa254..bf90cd0b 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index f791db14..dd9c3f8f 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "AccountData.h" #include #include diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 6749a471..092e1691 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index e404cdda..3422df7c 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "AccountList.h" diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index 025926ae..baaf7414 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp index 321b350f..49b6e46f 100644 --- a/launcher/minecraft/auth/AccountTask.cpp +++ b/launcher/minecraft/auth/AccountTask.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "AccountTask.h" diff --git a/launcher/minecraft/auth/AccountTask.h b/launcher/minecraft/auth/AccountTask.h index c2a5d86c..1bd6c59f 100644 --- a/launcher/minecraft/auth/AccountTask.h +++ b/launcher/minecraft/auth/AccountTask.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index a604cadf..ec86fa5c 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -1,18 +1,38 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * Authors: Orochimarufan + * 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. * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . * - * 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. + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Authors: Orochimarufan + * + * 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 "MinecraftAccount.h" diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 6592f9c0..7777f846 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index 207d9373..16afcb42 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "MSAStep.h" #include diff --git a/launcher/minecraft/auth/steps/MSAStep.h b/launcher/minecraft/auth/steps/MSAStep.h index 301e1465..e9a1524e 100644 --- a/launcher/minecraft/auth/steps/MSAStep.h +++ b/launcher/minecraft/auth/steps/MSAStep.h @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index f3bf7abe..5fac1015 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "ExportInstanceDialog.h" diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index 174ad46c..b11b6980 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "MSALoginDialog.h" diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index ad79e00c..037ec217 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC & PolyMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "APIPage.h" diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index 9474ebbb..d9a84753 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index eb1ee8d3..af1d9d60 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "AccountListPage.h" diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index 841c3fd2..c1dea8be 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 From 90780818cab043003cb4d18d3865323ce07fb92d Mon Sep 17 00:00:00 2001 From: Jim Ramsay Date: Thu, 17 Mar 2022 21:47:34 -0400 Subject: [PATCH 100/605] Limit offline username to 16 characters with override Offline usernames longer than 16 characters won't be able to connect to LAN games or offline-mode servers, so just don't let it happen. Add a checkbox to allow people to unrestrict usernames if they want. Signed-off-by: Jim Ramsay --- launcher/ui/dialogs/OfflineLoginDialog.cpp | 9 +++++++++ launcher/ui/dialogs/OfflineLoginDialog.h | 1 + launcher/ui/dialogs/OfflineLoginDialog.ui | 13 +++++++++++++ 3 files changed, 23 insertions(+) diff --git a/launcher/ui/dialogs/OfflineLoginDialog.cpp b/launcher/ui/dialogs/OfflineLoginDialog.cpp index 345ed40a..4f3d8be4 100644 --- a/launcher/ui/dialogs/OfflineLoginDialog.cpp +++ b/launcher/ui/dialogs/OfflineLoginDialog.cpp @@ -42,6 +42,15 @@ void OfflineLoginDialog::setUserInputsEnabled(bool 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) { diff --git a/launcher/ui/dialogs/OfflineLoginDialog.h b/launcher/ui/dialogs/OfflineLoginDialog.h index 5e608379..fdb3d91f 100644 --- a/launcher/ui/dialogs/OfflineLoginDialog.h +++ b/launcher/ui/dialogs/OfflineLoginDialog.h @@ -35,6 +35,7 @@ slots: void onTaskProgress(qint64 current, qint64 total); void on_userTextBox_textEdited(const QString &newText); + void on_allowLongUsernames_stateChanged(int value); private: Ui::OfflineLoginDialog *ui; diff --git a/launcher/ui/dialogs/OfflineLoginDialog.ui b/launcher/ui/dialogs/OfflineLoginDialog.ui index d8964a2e..4633cbe3 100644 --- a/launcher/ui/dialogs/OfflineLoginDialog.ui +++ b/launcher/ui/dialogs/OfflineLoginDialog.ui @@ -35,11 +35,24 @@
+ + 16 + Username + + + + Usernames longer than 16 characters cannot be used for LAN games or offline-mode servers. + + + Allow long usernames + + + From bb5a91c1794b32d7eb6d0572df0deb6c7f48998c Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sat, 19 Mar 2022 19:01:51 +0100 Subject: [PATCH 101/605] Update CMakeLists.txt --- launcher/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 751b354b..597b0b1d 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -984,7 +984,7 @@ if(INSTALL_BUNDLE STREQUAL "full") DIRECTORY "${QT_PLUGINS_DIR}/imageformats" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime - REGEX "tga|tiff|mng|webp" EXCLUDE + REGEX "tga|tiff|mng" EXCLUDE ) # Icon engines install( From 75ec4256e2ae27adeb7df0adfc56fff4a801bea6 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 19 Mar 2022 17:59:00 -0300 Subject: [PATCH 102/605] feat(ui): allow users to move toolbars to different places --- launcher/ui/MainWindow.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 7b758e05..ab4f11e8 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -273,8 +273,8 @@ public: { mainToolBar = TranslatedToolbar(MainWindow); mainToolBar->setObjectName(QStringLiteral("mainToolBar")); - mainToolBar->setMovable(false); - mainToolBar->setAllowedAreas(Qt::TopToolBarArea); + mainToolBar->setMovable(true); + mainToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); mainToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); mainToolBar->setFloatable(false); mainToolBar.setWindowTitleId(QT_TRANSLATE_NOOP("MainWindow", "Main Toolbar")); @@ -442,8 +442,8 @@ public: { newsToolBar = TranslatedToolbar(MainWindow); newsToolBar->setObjectName(QStringLiteral("newsToolBar")); - newsToolBar->setMovable(false); - newsToolBar->setAllowedAreas(Qt::BottomToolBarArea); + newsToolBar->setMovable(true); + newsToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); newsToolBar->setIconSize(QSize(16, 16)); newsToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); newsToolBar->setFloatable(false); @@ -467,6 +467,7 @@ public: instanceToolBar->setObjectName(QStringLiteral("instanceToolBar")); // disabled until we have an instance selected instanceToolBar->setEnabled(false); + instanceToolBar->setMovable(true); instanceToolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea); instanceToolBar->setToolButtonStyle(Qt::ToolButtonTextOnly); instanceToolBar->setFloatable(false); From c311dba465d0ad80ecde0493c308b587562ce392 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 19 Mar 2022 23:23:08 +0100 Subject: [PATCH 103/605] fix: limit instance names to 128 chars --- launcher/ui/dialogs/NewInstanceDialog.ui | 6 +++++- launcher/ui/instanceview/InstanceDelegate.cpp | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/NewInstanceDialog.ui b/launcher/ui/dialogs/NewInstanceDialog.ui index 7fb19ff5..8ca0b786 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.ui +++ b/launcher/ui/dialogs/NewInstanceDialog.ui @@ -44,7 +44,11 @@ - + + + 128 + + diff --git a/launcher/ui/instanceview/InstanceDelegate.cpp b/launcher/ui/instanceview/InstanceDelegate.cpp index 3c4ca63f..22ff78cd 100644 --- a/launcher/ui/instanceview/InstanceDelegate.cpp +++ b/launcher/ui/instanceview/InstanceDelegate.cpp @@ -405,6 +405,8 @@ void ListViewDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, QString text = realeditor->toPlainText(); text.replace(QChar('\n'), QChar(' ')); text = text.trimmed(); + // Prevent instance names longer than 128 chars + text.truncate(128); if(text.size() != 0) { model->setData(index, text); From b7f29593530d20e2fdfcce02c1f2cbd23ae81985 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sun, 20 Mar 2022 13:15:56 +0100 Subject: [PATCH 104/605] fix --- launcher/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 597b0b1d..6491ccce 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1014,7 +1014,7 @@ if(INSTALL_BUNDLE STREQUAL "full") DIRECTORY "${QT_PLUGINS_DIR}/imageformats" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime - REGEX "tga|tiff|mng|webp" EXCLUDE + REGEX "tga|tiff|mng" EXCLUDE REGEX "d\\." EXCLUDE REGEX "_debug\\." EXCLUDE REGEX "\\.dSYM" EXCLUDE From 17d200dc88d34df6ec27afa93c19798666d8bf7d Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 20 Mar 2022 14:56:21 +0100 Subject: [PATCH 105/605] chore(actions): add nightly.link comments --- .github/workflows/pr-comment.yml | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/pr-comment.yml diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml new file mode 100644 index 00000000..7e8e4d99 --- /dev/null +++ b/.github/workflows/pr-comment.yml @@ -0,0 +1,61 @@ +name: Comment on pull request +on: + workflow_run: + workflows: ['Test workflow with upload'] + types: [completed] +jobs: + pr_comment: + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v5 + with: + # This snippet is public-domain, taken from + # https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml + script: | + async function upsertComment(owner, repo, issue_number, purpose, body) { + const {data: comments} = await github.rest.issues.listComments( + {owner, repo, issue_number}); + + const marker = ``; + body = marker + "\n" + body; + + const existing = comments.filter((c) => c.body.includes(marker)); + if (existing.length > 0) { + const last = existing[existing.length - 1]; + core.info(`Updating comment ${last.id}`); + await github.rest.issues.updateComment({ + owner, repo, + body, + comment_id: last.id, + }); + } else { + core.info(`Creating a comment in issue / PR #${issue_number}`); + await github.rest.issues.createComment({issue_number, body, owner, repo}); + } + } + + const {owner, repo} = context.repo; + const run_id = ${{github.event.workflow_run.id}}; + + const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }}; + if (!pull_requests.length) { + return core.error("This workflow doesn't match any pull requests!"); + } + + const artifacts = await github.paginate( + github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id}); + if (!artifacts.length) { + return core.error(`No artifacts found`); + } + let body = `Download the artifacts for this pull request:\n`; + for (const art of artifacts) { + body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`; + } + + core.info("Review thread message body:", body); + + for (const pr of pull_requests) { + await upsertComment(owner, repo, pr.number, + "nightly-link", body); + } From 95182ed74b3cea178a113eb64d62cf9d5086ad74 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 20 Mar 2022 15:11:43 +0100 Subject: [PATCH 106/605] chore: bump version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7537703c..52170460 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,7 +53,7 @@ set(Launcher_HELP_URL "https://polymc.org/wiki/%1" CACHE STRING "URL (with arg % ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 1) set(Launcher_VERSION_MINOR 1) -set(Launcher_VERSION_HOTFIX 0) +set(Launcher_VERSION_HOTFIX 1) # Build number set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") From 2e40ab62441fd39a9811ba4d0f882dd7f22bec7a Mon Sep 17 00:00:00 2001 From: txtsd Date: Sun, 20 Mar 2022 11:30:56 +0530 Subject: [PATCH 107/605] (fix): Allow fractional DPI scaling --- launcher/main.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/launcher/main.cpp b/launcher/main.cpp index 8b572743..275fff32 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -29,6 +29,10 @@ int main(int argc, char *argv[]) QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); +#endif + // initialize Qt Application app(argc, argv); From 768007d9801593b3b41f948750c8b1dd520d004d Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 20 Mar 2022 15:34:13 +0100 Subject: [PATCH 108/605] fix: disable "Download mods" button when needed Fixes #271 --- launcher/ui/pages/instance/ModFolderPage.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index ffb87bbe..c7a0376d 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -244,10 +244,7 @@ void ModFolderPage::on_RunningState_changed(bool running) return; } m_controlsEnabled = !running; - ui->actionAdd->setEnabled(m_controlsEnabled); - ui->actionDisable->setEnabled(m_controlsEnabled); - ui->actionEnable->setEnabled(m_controlsEnabled); - ui->actionRemove->setEnabled(m_controlsEnabled); + ui->actionsToolbar->setEnabled(m_controlsEnabled); } bool ModFolderPage::shouldDisplay() const From 702a1da0acb84013f1b6294cfab9cb78f7a23daf Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 20 Mar 2022 15:35:35 +0100 Subject: [PATCH 109/605] fix: disable "Install Forge" button when needed --- launcher/ui/pages/instance/VersionPage.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 0fa5f68d..43c449eb 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -212,6 +212,8 @@ void VersionPage::updateVersionControls() // FIXME: this is a dirty hack auto minecraftVersion = Version(m_profile->getComponentVersion("net.minecraft")); + ui->actionInstall_Forge->setEnabled(controlsEnabled); + bool supportsFabric = minecraftVersion >= Version("1.14"); ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric); From a2c85a8531208194c992590fe4d89b853b057da2 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Tue, 22 Feb 2022 17:40:50 +0000 Subject: [PATCH 110/605] App: Retranslate page header titles This fixes a bug that is only practically effects the title of the language page not updating the header when changing the language. --- launcher/ui/widgets/PageContainer.cpp | 14 ++++++++++++++ launcher/ui/widgets/PageContainer.h | 3 +++ 2 files changed, 17 insertions(+) diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 558a98fb..b5a87f8b 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -162,6 +162,12 @@ void PageContainer::createUI() setLayout(m_layout); } +void PageContainer::retranslate() +{ + if (m_currentPage) + m_header->setText(m_currentPage->displayName()); +} + void PageContainer::addButtons(QWidget *buttons) { m_layout->addWidget(buttons, 2, 0, 1, 2); @@ -239,3 +245,11 @@ bool PageContainer::saveAll() } return true; } + +void PageContainer::changeEvent(QEvent* event) +{ + if (event->type() == QEvent::LanguageChange) { + retranslate(); + } + QWidget::changeEvent(event); +} diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h index 8d2172db..b5d0e3c1 100644 --- a/launcher/ui/widgets/PageContainer.h +++ b/launcher/ui/widgets/PageContainer.h @@ -66,8 +66,11 @@ public: m_container = container; }; + void changeEvent(QEvent*) override; + private: void createUI(); + void retranslate(); public slots: void help(); From dd5c4b6864397184152d8e7183cb7c97ae4e65db Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Tue, 22 Feb 2022 18:23:53 +0000 Subject: [PATCH 111/605] App: Retranslate all pages when the language is changed --- launcher/ui/pages/BasePage.h | 2 ++ launcher/ui/pages/global/APIPage.cpp | 5 +++++ launcher/ui/pages/global/APIPage.h | 1 + launcher/ui/pages/global/AccountListPage.cpp | 5 +++++ launcher/ui/pages/global/AccountListPage.h | 1 + launcher/ui/pages/global/CustomCommandsPage.cpp | 5 +++++ launcher/ui/pages/global/CustomCommandsPage.h | 1 + launcher/ui/pages/global/ExternalToolsPage.cpp | 5 +++++ launcher/ui/pages/global/ExternalToolsPage.h | 1 + launcher/ui/pages/global/JavaPage.cpp | 5 +++++ launcher/ui/pages/global/JavaPage.h | 1 + launcher/ui/pages/global/LanguagePage.cpp | 9 --------- launcher/ui/pages/global/LanguagePage.h | 3 +-- launcher/ui/pages/global/LauncherPage.cpp | 5 +++++ launcher/ui/pages/global/LauncherPage.h | 1 + launcher/ui/pages/global/MinecraftPage.cpp | 5 +++++ launcher/ui/pages/global/MinecraftPage.h | 1 + launcher/ui/pages/global/ProxyPage.cpp | 5 +++++ launcher/ui/pages/global/ProxyPage.h | 1 + launcher/ui/pages/instance/GameOptionsPage.cpp | 5 +++++ launcher/ui/pages/instance/GameOptionsPage.h | 1 + launcher/ui/pages/instance/InstanceSettingsPage.cpp | 5 +++++ launcher/ui/pages/instance/InstanceSettingsPage.h | 1 + launcher/ui/pages/instance/LogPage.cpp | 5 +++++ launcher/ui/pages/instance/LogPage.h | 1 + launcher/ui/pages/instance/ModFolderPage.cpp | 5 +++++ launcher/ui/pages/instance/ModFolderPage.h | 1 + launcher/ui/pages/instance/NotesPage.cpp | 5 +++++ launcher/ui/pages/instance/NotesPage.h | 1 + launcher/ui/pages/instance/OtherLogsPage.cpp | 5 +++++ launcher/ui/pages/instance/OtherLogsPage.h | 2 ++ launcher/ui/pages/instance/ScreenshotsPage.cpp | 5 +++++ launcher/ui/pages/instance/ScreenshotsPage.h | 1 + launcher/ui/pages/instance/ServersPage.cpp | 5 +++++ launcher/ui/pages/instance/ServersPage.h | 1 + launcher/ui/pages/instance/VersionPage.cpp | 5 +++++ launcher/ui/pages/instance/VersionPage.h | 1 + launcher/ui/pages/instance/WorldListPage.cpp | 5 +++++ launcher/ui/pages/instance/WorldListPage.h | 1 + launcher/ui/pages/modplatform/ImportPage.cpp | 5 +++++ launcher/ui/pages/modplatform/ImportPage.h | 1 + launcher/ui/pages/modplatform/VanillaPage.cpp | 5 +++++ launcher/ui/pages/modplatform/VanillaPage.h | 2 ++ launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp | 5 +++++ launcher/ui/pages/modplatform/atlauncher/AtlPage.h | 1 + launcher/ui/pages/modplatform/flame/FlamePage.cpp | 5 +++++ launcher/ui/pages/modplatform/flame/FlamePage.h | 1 + launcher/ui/pages/modplatform/ftb/FtbPage.cpp | 5 +++++ launcher/ui/pages/modplatform/ftb/FtbPage.h | 1 + launcher/ui/pages/modplatform/legacy_ftb/Page.cpp | 5 +++++ launcher/ui/pages/modplatform/legacy_ftb/Page.h | 1 + launcher/ui/pages/modplatform/technic/TechnicPage.cpp | 5 +++++ launcher/ui/pages/modplatform/technic/TechnicPage.h | 1 + launcher/ui/widgets/PageContainer.cpp | 3 +++ 54 files changed, 158 insertions(+), 11 deletions(-) diff --git a/launcher/ui/pages/BasePage.h b/launcher/ui/pages/BasePage.h index 408965d0..7af65ddf 100644 --- a/launcher/ui/pages/BasePage.h +++ b/launcher/ui/pages/BasePage.h @@ -47,6 +47,8 @@ public: { m_container = container; }; + virtual void retranslate() { } + public: int stackIndex = -1; int listIndex = -1; diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 037ec217..83a778fa 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -85,3 +85,8 @@ bool APIPage::apply() applySettings(); return true; } + +void APIPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index d9a84753..54c9d292 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -69,6 +69,7 @@ public: return "APIs"; } virtual bool apply() override; + void retranslate() override; private: void loadSettings(); diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index af1d9d60..8d3c8e8c 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -104,6 +104,11 @@ AccountListPage::~AccountListPage() delete ui; } +void AccountListPage::retranslate() +{ + ui->retranslateUi(this); +} + void AccountListPage::ShowContextMenu(const QPoint& pos) { auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index c1dea8be..2f5c2ef3 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -78,6 +78,7 @@ public: { return "Getting-Started#adding-an-account"; } + void retranslate() override; public slots: void on_actionAddMojang_triggered(); diff --git a/launcher/ui/pages/global/CustomCommandsPage.cpp b/launcher/ui/pages/global/CustomCommandsPage.cpp index 8541e3c1..b58d2c15 100644 --- a/launcher/ui/pages/global/CustomCommandsPage.cpp +++ b/launcher/ui/pages/global/CustomCommandsPage.cpp @@ -49,3 +49,8 @@ void CustomCommandsPage::loadSettings() s->get("PostExitCommand").toString() ); } + +void CustomCommandsPage::retranslate() +{ + // fixme: implement +} diff --git a/launcher/ui/pages/global/CustomCommandsPage.h b/launcher/ui/pages/global/CustomCommandsPage.h index a1155e0e..ea95c08a 100644 --- a/launcher/ui/pages/global/CustomCommandsPage.h +++ b/launcher/ui/pages/global/CustomCommandsPage.h @@ -47,6 +47,7 @@ public: return "Custom-commands"; } bool apply() override; + void retranslate() override; private: void applySettings(); diff --git a/launcher/ui/pages/global/ExternalToolsPage.cpp b/launcher/ui/pages/global/ExternalToolsPage.cpp index 41d900aa..40beccbc 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.cpp +++ b/launcher/ui/pages/global/ExternalToolsPage.cpp @@ -231,3 +231,8 @@ bool ExternalToolsPage::apply() applySettings(); return true; } + +void ExternalToolsPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/global/ExternalToolsPage.h b/launcher/ui/pages/global/ExternalToolsPage.h index 5ae6148d..0c6f2a77 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.h +++ b/launcher/ui/pages/global/ExternalToolsPage.h @@ -54,6 +54,7 @@ public: return "Tools"; } virtual bool apply() override; + void retranslate() override; private: void loadSettings(); diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index bd79f11a..59f5213c 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -151,3 +151,8 @@ void JavaPage::checkerFinished() { checker.reset(); } + +void JavaPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/global/JavaPage.h b/launcher/ui/pages/global/JavaPage.h index 8f9b3323..8396695b 100644 --- a/launcher/ui/pages/global/JavaPage.h +++ b/launcher/ui/pages/global/JavaPage.h @@ -54,6 +54,7 @@ public: return "Java-settings"; } bool apply() override; + void retranslate() override; private: void applySettings(); diff --git a/launcher/ui/pages/global/LanguagePage.cpp b/launcher/ui/pages/global/LanguagePage.cpp index 359fdeeb..a4e6fd1e 100644 --- a/launcher/ui/pages/global/LanguagePage.cpp +++ b/launcher/ui/pages/global/LanguagePage.cpp @@ -40,12 +40,3 @@ void LanguagePage::retranslate() { mainWidget->retranslate(); } - -void LanguagePage::changeEvent(QEvent* event) -{ - if (event->type() == QEvent::LanguageChange) - { - retranslate(); - } - QWidget::changeEvent(event); -} diff --git a/launcher/ui/pages/global/LanguagePage.h b/launcher/ui/pages/global/LanguagePage.h index b1dd05ad..7f870c5d 100644 --- a/launcher/ui/pages/global/LanguagePage.h +++ b/launcher/ui/pages/global/LanguagePage.h @@ -48,12 +48,11 @@ public: } bool apply() override; - void changeEvent(QEvent * ) override; + void retranslate() override; private: void applySettings(); void loadSettings(); - void retranslate(); private: LanguageSelectionWidget *mainWidget; diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index ee68cd08..98271516 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -441,3 +441,8 @@ void LauncherPage::refreshFontPreview() workCursor.insertBlock(); } } + +void LauncherPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index 4d0cf3c9..c142554f 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -57,6 +57,7 @@ public: return "Launcher-settings"; } bool apply() override; + void retranslate() override; private: void applySettings(); diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index 5470a586..1c7de39e 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -94,3 +94,8 @@ void MinecraftPage::loadSettings() ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool()); } + +void MinecraftPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/global/MinecraftPage.h b/launcher/ui/pages/global/MinecraftPage.h index 42626d94..61a25ba8 100644 --- a/launcher/ui/pages/global/MinecraftPage.h +++ b/launcher/ui/pages/global/MinecraftPage.h @@ -54,6 +54,7 @@ public: return "Minecraft-settings"; } bool apply() override; + void retranslate() override; private: void updateCheckboxStuff(); diff --git a/launcher/ui/pages/global/ProxyPage.cpp b/launcher/ui/pages/global/ProxyPage.cpp index 5bc8199e..77270399 100644 --- a/launcher/ui/pages/global/ProxyPage.cpp +++ b/launcher/ui/pages/global/ProxyPage.cpp @@ -104,3 +104,8 @@ void ProxyPage::loadSettings() ui->proxyUserEdit->setText(s->get("ProxyUser").toString()); ui->proxyPassEdit->setText(s->get("ProxyPass").toString()); } + +void ProxyPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/global/ProxyPage.h b/launcher/ui/pages/global/ProxyPage.h index 6698c349..91173c44 100644 --- a/launcher/ui/pages/global/ProxyPage.h +++ b/launcher/ui/pages/global/ProxyPage.h @@ -51,6 +51,7 @@ public: return "Proxy-settings"; } bool apply() override; + void retranslate() override; private: void updateCheckboxStuff(); diff --git a/launcher/ui/pages/instance/GameOptionsPage.cpp b/launcher/ui/pages/instance/GameOptionsPage.cpp index 782f2ab3..485a84bb 100644 --- a/launcher/ui/pages/instance/GameOptionsPage.cpp +++ b/launcher/ui/pages/instance/GameOptionsPage.cpp @@ -35,3 +35,8 @@ void GameOptionsPage::closedImpl() { // m_model->unobserve(); } + +void GameOptionsPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/instance/GameOptionsPage.h b/launcher/ui/pages/instance/GameOptionsPage.h index 878903eb..a3a2e014 100644 --- a/launcher/ui/pages/instance/GameOptionsPage.h +++ b/launcher/ui/pages/instance/GameOptionsPage.h @@ -56,6 +56,7 @@ public: { return "Game-Options-management"; } + void retranslate() override; private: // data Ui::GameOptionsPage *ui = nullptr; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index b0e18af4..573ebc3e 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -339,3 +339,8 @@ void InstanceSettingsPage::checkerFinished() { checker.reset(); } + +void InstanceSettingsPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 5c8c8e66..9ea062b9 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -55,6 +55,7 @@ public: return "Instance-settings"; } virtual bool shouldDisplay() const override; + void retranslate() override; private slots: void on_javaDetectBtn_clicked(); diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index b66c6cc7..362ca392 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -328,3 +328,8 @@ void LogPage::findActivated() ui->searchBar->selectAll(); } } + +void LogPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h index cab25563..519756b0 100644 --- a/launcher/ui/pages/instance/LogPage.h +++ b/launcher/ui/pages/instance/LogPage.h @@ -54,6 +54,7 @@ public: return "Minecraft-Logs"; } virtual bool shouldDisplay() const override; + void retranslate() override; private slots: void on_btnPaste_clicked(); diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index c7a0376d..e474b821 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -252,6 +252,11 @@ bool ModFolderPage::shouldDisplay() const return true; } +void ModFolderPage::retranslate() +{ + ui->retranslateUi(this); +} + bool CoreModFolderPage::shouldDisplay() const { if (ModFolderPage::shouldDisplay()) diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index fbda3cd8..951d0ee3 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -66,6 +66,7 @@ public: return m_helpName; } virtual bool shouldDisplay() const override; + void retranslate() override; virtual void openedImpl() override; virtual void closedImpl() override; diff --git a/launcher/ui/pages/instance/NotesPage.cpp b/launcher/ui/pages/instance/NotesPage.cpp index fa966c91..9fe8472e 100644 --- a/launcher/ui/pages/instance/NotesPage.cpp +++ b/launcher/ui/pages/instance/NotesPage.cpp @@ -19,3 +19,8 @@ bool NotesPage::apply() m_inst->setNotes(ui->noteEditor->toPlainText()); return true; } + +void NotesPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/instance/NotesPage.h b/launcher/ui/pages/instance/NotesPage.h index 539401ee..d5c69701 100644 --- a/launcher/ui/pages/instance/NotesPage.h +++ b/launcher/ui/pages/instance/NotesPage.h @@ -53,6 +53,7 @@ public: { return "Notes"; } + void retranslate() override; private: Ui::NotesPage *ui; diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 0131c5c1..9900c29f 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -55,6 +55,11 @@ OtherLogsPage::~OtherLogsPage() delete ui; } +void OtherLogsPage::retranslate() +{ + ui->retranslateUi(this); +} + void OtherLogsPage::openedImpl() { m_watcher->enable(); diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index b2b2a91b..2d22e07b 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -52,6 +52,8 @@ public: { return "Minecraft-Logs"; } + void retranslate() override; + void openedImpl() override; void closedImpl() override; diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index 4011d88c..fadf5859 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -270,6 +270,11 @@ bool ScreenshotsPage::eventFilter(QObject *obj, QEvent *evt) return QWidget::eventFilter(obj, evt); } +void ScreenshotsPage::retranslate() +{ + ui->retranslateUi(this); +} + ScreenshotsPage::~ScreenshotsPage() { delete ui; diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index 2a1fdeee..328f3de4 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -67,6 +67,7 @@ public: { return !m_uploadActive; } + void retranslate() override; protected: QMenu * createPopupMenu() override; diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 8116d2bf..150b477b 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -600,6 +600,11 @@ ServersPage::~ServersPage() delete ui; } +void ServersPage::retranslate() +{ + ui->retranslateUi(this); +} + void ServersPage::ShowContextMenu(const QPoint& pos) { auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h index d91da2ae..129bd410 100644 --- a/launcher/ui/pages/instance/ServersPage.h +++ b/launcher/ui/pages/instance/ServersPage.h @@ -57,6 +57,7 @@ public: { return "Servers-management"; } + void retranslate() override; protected: QMenu * createPopupMenu() override; diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 43c449eb..396ba12c 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -99,6 +99,11 @@ bool VersionPage::shouldDisplay() const return true; } +void VersionPage::retranslate() +{ + ui->retranslateUi(this); +} + QMenu * VersionPage::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index b5ce4064..873716bc 100644 --- a/launcher/ui/pages/instance/VersionPage.h +++ b/launcher/ui/pages/instance/VersionPage.h @@ -47,6 +47,7 @@ public: return "Instance-Version"; } virtual bool shouldDisplay() const override; + void retranslate() override; private slots: void on_actionChange_version_triggered(); diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index d2bf63bd..50795db6 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -122,6 +122,11 @@ bool WorldListPage::shouldDisplay() const return true; } +void WorldListPage::retranslate() +{ + ui->retranslateUi(this); +} + bool WorldListPage::worldListFilter(QKeyEvent *keyEvent) { switch (keyEvent->key()) diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h index e07d5794..25a228d9 100644 --- a/launcher/ui/pages/instance/WorldListPage.h +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -57,6 +57,7 @@ public: return "Worlds"; } virtual bool shouldDisplay() const override; + void retranslate() override; virtual void openedImpl() override; virtual void closedImpl() override; diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index c9e24ead..9124abc1 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -50,6 +50,11 @@ bool ImportPage::shouldDisplay() const return true; } +void ImportPage::retranslate() +{ + ui->retranslateUi(this); +} + void ImportPage::openedImpl() { updateState(); diff --git a/launcher/ui/pages/modplatform/ImportPage.h b/launcher/ui/pages/modplatform/ImportPage.h index aba4def0..36772717 100644 --- a/launcher/ui/pages/modplatform/ImportPage.h +++ b/launcher/ui/pages/modplatform/ImportPage.h @@ -52,6 +52,7 @@ public: return "Zip-import"; } virtual bool shouldDisplay() const override; + void retranslate() override; void setUrl(const QString & url); void openedImpl() override; diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/VanillaPage.cpp index 5c58c1f1..57545a5f 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.cpp +++ b/launcher/ui/pages/modplatform/VanillaPage.cpp @@ -74,6 +74,11 @@ bool VanillaPage::shouldDisplay() const return true; } +void VanillaPage::retranslate() +{ + ui->retranslateUi(this); +} + BaseVersionPtr VanillaPage::selectedVersion() const { return m_selectedVersion; diff --git a/launcher/ui/pages/modplatform/VanillaPage.h b/launcher/ui/pages/modplatform/VanillaPage.h index fd4c2daa..8c712a64 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.h +++ b/launcher/ui/pages/modplatform/VanillaPage.h @@ -52,6 +52,8 @@ public: return "Vanilla-platform"; } virtual bool shouldDisplay() const override; + void retranslate() override; + void openedImpl() override; BaseVersionPtr selectedVersion() const; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index af0cc8d6..bdd6369f 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -65,6 +65,11 @@ bool AtlPage::shouldDisplay() const return true; } +void AtlPage::retranslate() +{ + ui->retranslateUi(this); +} + void AtlPage::openedImpl() { if(!initialized) diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index 5b3f2228..958d8724 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -57,6 +57,7 @@ public: return "ATL-platform"; } virtual bool shouldDisplay() const override; + void retranslate() override; void openedImpl() override; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 7e6ac2fd..24aa2f53 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -57,6 +57,11 @@ bool FlamePage::shouldDisplay() const return true; } +void FlamePage::retranslate() +{ + ui->retranslateUi(this); +} + void FlamePage::openedImpl() { suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 5cfe21dc..7735bf90 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -57,6 +57,7 @@ public: return "Flame-platform"; } virtual bool shouldDisplay() const override; + void retranslate() override; void openedImpl() override; diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp index b6b5dcd4..e2f3a99d 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp @@ -78,6 +78,11 @@ bool FtbPage::shouldDisplay() const return true; } +void FtbPage::retranslate() +{ + ui->retranslateUi(this); +} + void FtbPage::openedImpl() { if(!initialised) diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.h b/launcher/ui/pages/modplatform/ftb/FtbPage.h index 28a189f0..5f20832d 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.h +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.h @@ -55,6 +55,7 @@ public: return "FTB-platform"; } virtual bool shouldDisplay() const override; + void retranslate() override; void openedImpl() override; diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 891704de..9670d294 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -122,6 +122,11 @@ void Page::openedImpl() suggestCurrent(); } +void Page::retranslate() +{ + ui->retranslateUi(this); +} + void Page::suggestCurrent() { if(!isOpened) diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index d8225e11..c8c7a48d 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -66,6 +66,7 @@ public: } bool shouldDisplay() const override; void openedImpl() override; + void retranslate() override; private: void suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 67f6e52c..141a558e 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -61,6 +61,11 @@ bool TechnicPage::shouldDisplay() const return true; } +void TechnicPage::retranslate() +{ + ui->retranslateUi(this); +} + void TechnicPage::openedImpl() { suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index 21695dd0..a2a25b21 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -57,6 +57,7 @@ public: return "Technic-platform"; } virtual bool shouldDisplay() const override; + void retranslate() override; void openedImpl() override; diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index b5a87f8b..4d84f4ab 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -166,6 +166,9 @@ void PageContainer::retranslate() { if (m_currentPage) m_header->setText(m_currentPage->displayName()); + + for (auto page : m_model->pages()) + page->retranslate(); } void PageContainer::addButtons(QWidget *buttons) From cafff5e504b286a1fe650899cedff2b3542ee4c5 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 20 Mar 2022 20:01:08 +0100 Subject: [PATCH 112/605] chore: add license header --- launcher/ui/pages/BasePage.h | 40 +++++++++++++----- launcher/ui/pages/global/APIPage.cpp | 1 + launcher/ui/pages/global/APIPage.h | 1 + launcher/ui/pages/global/AccountListPage.cpp | 1 + launcher/ui/pages/global/AccountListPage.h | 1 + .../ui/pages/global/CustomCommandsPage.cpp | 35 ++++++++++++++++ launcher/ui/pages/global/CustomCommandsPage.h | 40 +++++++++++++----- .../ui/pages/global/ExternalToolsPage.cpp | 40 +++++++++++++----- launcher/ui/pages/global/ExternalToolsPage.h | 40 +++++++++++++----- launcher/ui/pages/global/JavaPage.cpp | 40 +++++++++++++----- launcher/ui/pages/global/JavaPage.h | 40 +++++++++++++----- launcher/ui/pages/global/LanguagePage.cpp | 35 ++++++++++++++++ launcher/ui/pages/global/LanguagePage.h | 40 +++++++++++++----- launcher/ui/pages/global/LauncherPage.cpp | 40 +++++++++++++----- launcher/ui/pages/global/LauncherPage.h | 40 +++++++++++++----- launcher/ui/pages/global/MinecraftPage.cpp | 40 +++++++++++++----- launcher/ui/pages/global/MinecraftPage.h | 40 +++++++++++++----- launcher/ui/pages/global/ProxyPage.cpp | 40 +++++++++++++----- launcher/ui/pages/global/ProxyPage.h | 40 +++++++++++++----- .../ui/pages/instance/GameOptionsPage.cpp | 35 ++++++++++++++++ launcher/ui/pages/instance/GameOptionsPage.h | 40 +++++++++++++----- .../pages/instance/InstanceSettingsPage.cpp | 35 ++++++++++++++++ .../ui/pages/instance/InstanceSettingsPage.h | 40 +++++++++++++----- launcher/ui/pages/instance/LogPage.cpp | 35 ++++++++++++++++ launcher/ui/pages/instance/LogPage.h | 40 +++++++++++++----- launcher/ui/pages/instance/ModFolderPage.cpp | 40 +++++++++++++----- launcher/ui/pages/instance/ModFolderPage.h | 40 +++++++++++++----- launcher/ui/pages/instance/NotesPage.cpp | 35 ++++++++++++++++ launcher/ui/pages/instance/NotesPage.h | 40 +++++++++++++----- launcher/ui/pages/instance/OtherLogsPage.cpp | 40 +++++++++++++----- launcher/ui/pages/instance/OtherLogsPage.h | 40 +++++++++++++----- launcher/ui/pages/instance/ResourcePackPage.h | 35 ++++++++++++++++ .../ui/pages/instance/ScreenshotsPage.cpp | 35 ++++++++++++++++ launcher/ui/pages/instance/ScreenshotsPage.h | 40 +++++++++++++----- launcher/ui/pages/instance/ServersPage.cpp | 35 ++++++++++++++++ launcher/ui/pages/instance/ServersPage.h | 40 +++++++++++++----- launcher/ui/pages/instance/ShaderPackPage.h | 35 ++++++++++++++++ launcher/ui/pages/instance/TexturePackPage.h | 35 ++++++++++++++++ launcher/ui/pages/instance/VersionPage.cpp | 40 +++++++++++++----- launcher/ui/pages/instance/VersionPage.h | 40 +++++++++++++----- launcher/ui/pages/instance/WorldListPage.cpp | 40 +++++++++++++----- launcher/ui/pages/instance/WorldListPage.h | 40 +++++++++++++----- launcher/ui/pages/modplatform/ImportPage.cpp | 35 ++++++++++++++++ launcher/ui/pages/modplatform/ImportPage.h | 40 +++++++++++++----- launcher/ui/pages/modplatform/VanillaPage.cpp | 35 ++++++++++++++++ launcher/ui/pages/modplatform/VanillaPage.h | 40 +++++++++++++----- .../pages/modplatform/atlauncher/AtlPage.cpp | 41 ++++++++++++++----- .../ui/pages/modplatform/atlauncher/AtlPage.h | 39 +++++++++++++----- .../ui/pages/modplatform/flame/FlamePage.cpp | 35 ++++++++++++++++ .../ui/pages/modplatform/flame/FlamePage.h | 40 +++++++++++++----- launcher/ui/pages/modplatform/ftb/FtbPage.cpp | 41 ++++++++++++++----- launcher/ui/pages/modplatform/ftb/FtbPage.h | 40 +++++++++++++----- .../ui/pages/modplatform/legacy_ftb/Page.cpp | 35 ++++++++++++++++ .../ui/pages/modplatform/legacy_ftb/Page.h | 40 +++++++++++++----- .../pages/modplatform/technic/TechnicPage.cpp | 40 +++++++++++++----- .../pages/modplatform/technic/TechnicPage.h | 40 +++++++++++++----- launcher/ui/widgets/PageContainer.cpp | 41 ++++++++++++++----- launcher/ui/widgets/PageContainer.h | 40 +++++++++++++----- 58 files changed, 1699 insertions(+), 392 deletions(-) diff --git a/launcher/ui/pages/BasePage.h b/launcher/ui/pages/BasePage.h index 7af65ddf..ceb24040 100644 --- a/launcher/ui/pages/BasePage.h +++ b/launcher/ui/pages/BasePage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 83a778fa..287eb74f 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * 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 diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index 54c9d292..20356009 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * 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 diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 8d3c8e8c..1edba499 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * 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 diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index 2f5c2ef3..9395e92b 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * 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 diff --git a/launcher/ui/pages/global/CustomCommandsPage.cpp b/launcher/ui/pages/global/CustomCommandsPage.cpp index b58d2c15..65544d5c 100644 --- a/launcher/ui/pages/global/CustomCommandsPage.cpp +++ b/launcher/ui/pages/global/CustomCommandsPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "CustomCommandsPage.h" #include #include diff --git a/launcher/ui/pages/global/CustomCommandsPage.h b/launcher/ui/pages/global/CustomCommandsPage.h index ea95c08a..865503ff 100644 --- a/launcher/ui/pages/global/CustomCommandsPage.h +++ b/launcher/ui/pages/global/CustomCommandsPage.h @@ -1,16 +1,36 @@ -/* Copyright 2018-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/global/ExternalToolsPage.cpp b/launcher/ui/pages/global/ExternalToolsPage.cpp index 40beccbc..693ca5c1 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.cpp +++ b/launcher/ui/pages/global/ExternalToolsPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "ExternalToolsPage.h" diff --git a/launcher/ui/pages/global/ExternalToolsPage.h b/launcher/ui/pages/global/ExternalToolsPage.h index 0c6f2a77..8bd38a19 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.h +++ b/launcher/ui/pages/global/ExternalToolsPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index 59f5213c..3eb4bd59 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "JavaPage.h" diff --git a/launcher/ui/pages/global/JavaPage.h b/launcher/ui/pages/global/JavaPage.h index 8396695b..64d4098e 100644 --- a/launcher/ui/pages/global/JavaPage.h +++ b/launcher/ui/pages/global/JavaPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/global/LanguagePage.cpp b/launcher/ui/pages/global/LanguagePage.cpp index a4e6fd1e..485d7fd4 100644 --- a/launcher/ui/pages/global/LanguagePage.cpp +++ b/launcher/ui/pages/global/LanguagePage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "LanguagePage.h" #include "ui/widgets/LanguageSelectionWidget.h" diff --git a/launcher/ui/pages/global/LanguagePage.h b/launcher/ui/pages/global/LanguagePage.h index 7f870c5d..9b321170 100644 --- a/launcher/ui/pages/global/LanguagePage.h +++ b/launcher/ui/pages/global/LanguagePage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 98271516..6f7e1cc7 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "LauncherPage.h" diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index c142554f..63cfe9c3 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index 1c7de39e..9abae425 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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" diff --git a/launcher/ui/pages/global/MinecraftPage.h b/launcher/ui/pages/global/MinecraftPage.h index 61a25ba8..cf5f95eb 100644 --- a/launcher/ui/pages/global/MinecraftPage.h +++ b/launcher/ui/pages/global/MinecraftPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/global/ProxyPage.cpp b/launcher/ui/pages/global/ProxyPage.cpp index 77270399..aefd1e74 100644 --- a/launcher/ui/pages/global/ProxyPage.cpp +++ b/launcher/ui/pages/global/ProxyPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "ProxyPage.h" diff --git a/launcher/ui/pages/global/ProxyPage.h b/launcher/ui/pages/global/ProxyPage.h index 91173c44..e3677774 100644 --- a/launcher/ui/pages/global/ProxyPage.h +++ b/launcher/ui/pages/global/ProxyPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/instance/GameOptionsPage.cpp b/launcher/ui/pages/instance/GameOptionsPage.cpp index 485a84bb..63443166 100644 --- a/launcher/ui/pages/instance/GameOptionsPage.cpp +++ b/launcher/ui/pages/instance/GameOptionsPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "GameOptionsPage.h" #include "ui_GameOptionsPage.h" #include "minecraft/MinecraftInstance.h" diff --git a/launcher/ui/pages/instance/GameOptionsPage.h b/launcher/ui/pages/instance/GameOptionsPage.h index a3a2e014..de8c421e 100644 --- a/launcher/ui/pages/instance/GameOptionsPage.h +++ b/launcher/ui/pages/instance/GameOptionsPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 573ebc3e..fc8a2cff 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "InstanceSettingsPage.h" #include "ui_InstanceSettingsPage.h" diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 9ea062b9..97d1296f 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 362ca392..30a8735f 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "LogPage.h" #include "ui_LogPage.h" diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h index 519756b0..f6fe87c4 100644 --- a/launcher/ui/pages/instance/LogPage.h +++ b/launcher/ui/pages/instance/LogPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index e474b821..599f0e11 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "ModFolderPage.h" diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 951d0ee3..72e2d404 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/instance/NotesPage.cpp b/launcher/ui/pages/instance/NotesPage.cpp index 9fe8472e..95a9fad2 100644 --- a/launcher/ui/pages/instance/NotesPage.cpp +++ b/launcher/ui/pages/instance/NotesPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "NotesPage.h" #include "ui_NotesPage.h" #include diff --git a/launcher/ui/pages/instance/NotesPage.h b/launcher/ui/pages/instance/NotesPage.h index d5c69701..80a7279b 100644 --- a/launcher/ui/pages/instance/NotesPage.h +++ b/launcher/ui/pages/instance/NotesPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 9900c29f..0c1939c6 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "OtherLogsPage.h" diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index 2d22e07b..95591638 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h index 1486bf52..8054926c 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.h +++ b/launcher/ui/pages/instance/ResourcePackPage.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "ModFolderPage.h" diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index fadf5859..e694ebe3 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ScreenshotsPage.h" #include "ui_ScreenshotsPage.h" diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index 328f3de4..50cf1a17 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 150b477b..2af6164c 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ServersPage.h" #include "ui_ServersPage.h" diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h index 129bd410..5173712c 100644 --- a/launcher/ui/pages/instance/ServersPage.h +++ b/launcher/ui/pages/instance/ServersPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h index 36724992..7d4f5074 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.h +++ b/launcher/ui/pages/instance/ShaderPackPage.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "ModFolderPage.h" diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h index 3f04997d..e8cefe6e 100644 --- a/launcher/ui/pages/instance/TexturePackPage.h +++ b/launcher/ui/pages/instance/TexturePackPage.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "ModFolderPage.h" diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 396ba12c..97c6fe8f 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "Application.h" diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index 873716bc..2d37af43 100644 --- a/launcher/ui/pages/instance/VersionPage.h +++ b/launcher/ui/pages/instance/VersionPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 50795db6..650583a2 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2015-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "WorldListPage.h" diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h index 25a228d9..17e36a08 100644 --- a/launcher/ui/pages/instance/WorldListPage.h +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -1,16 +1,36 @@ -/* Copyright 2015-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index 9124abc1..487bf77b 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ImportPage.h" #include "ui_ImportPage.h" diff --git a/launcher/ui/pages/modplatform/ImportPage.h b/launcher/ui/pages/modplatform/ImportPage.h index 36772717..8d13ac10 100644 --- a/launcher/ui/pages/modplatform/ImportPage.h +++ b/launcher/ui/pages/modplatform/ImportPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/VanillaPage.cpp index 57545a5f..c691128f 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.cpp +++ b/launcher/ui/pages/modplatform/VanillaPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "VanillaPage.h" #include "ui_VanillaPage.h" diff --git a/launcher/ui/pages/modplatform/VanillaPage.h b/launcher/ui/pages/modplatform/VanillaPage.h index 8c712a64..4e7479df 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.h +++ b/launcher/ui/pages/modplatform/VanillaPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index bdd6369f..df9b9207 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield - * Copyright 2021 Philip T + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Philip T + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "AtlPage.h" diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index 958d8724..d5f622aa 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -1,17 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * + * 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 diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 24aa2f53..cbe709c2 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "FlamePage.h" #include "ui_FlamePage.h" diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 7735bf90..1695777a 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp index e2f3a99d..8a93bc2e 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield - * Copyright 2021 Philip T + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Philip T + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "FtbPage.h" diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.h b/launcher/ui/pages/modplatform/ftb/FtbPage.h index 5f20832d..e2dbca02 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.h +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 9670d294..27a12cda 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "Page.h" #include "ui_Page.h" diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index c8c7a48d..6f36e89b 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 141a558e..c3807269 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "TechnicPage.h" diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index a2a25b21..24dd2935 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 4d84f4ab..2af7d731 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -1,16 +1,37 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "PageContainer.h" diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h index b5d0e3c1..86f549eb 100644 --- a/launcher/ui/widgets/PageContainer.h +++ b/launcher/ui/widgets/PageContainer.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 From 536b1a23fc95626c670f25abe66940c1481713bc Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 20 Mar 2022 20:45:58 +0100 Subject: [PATCH 113/605] fix: retranslate mod download pages --- .../pages/modplatform/flame/FlameModPage.cpp | 40 +++++++++++++++++++ .../ui/pages/modplatform/flame/FlameModPage.h | 36 +++++++++++++++++ .../modplatform/modrinth/ModrinthPage.cpp | 39 ++++++++++++++++++ .../pages/modplatform/modrinth/ModrinthPage.h | 36 +++++++++++++++++ 4 files changed, 151 insertions(+) diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 114ac907..a4dc1088 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "FlameModPage.h" #include "ui_FlameModPage.h" @@ -60,6 +95,11 @@ bool FlameModPage::eventFilter(QObject *watched, QEvent *event) { bool FlameModPage::shouldDisplay() const { return true; } +void FlameModPage::retranslate() +{ + ui->retranslateUi(this); +} + void FlameModPage::openedImpl() { updateSelectionButton(); triggerSearch(); diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index b5b19a4f..e0ceb69c 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 @@ -42,6 +77,7 @@ public: return "Flame-platform"; } virtual bool shouldDisplay() const override; + void retranslate() override; void openedImpl() override; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 35cd743a..2643c313 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "ModrinthPage.h" #include "ui_ModrinthPage.h" @@ -59,6 +94,10 @@ bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) { bool ModrinthPage::shouldDisplay() const { return true; } +void ModrinthPage::retranslate() { + ui->retranslateUi(this); +} + void ModrinthPage::openedImpl() { updateSelectionButton(); triggerSearch(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 52b538e3..5002ae42 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 @@ -42,6 +77,7 @@ public: return "Modrinth-platform"; } virtual bool shouldDisplay() const override; + void retranslate() override; void openedImpl() override; From 75d0078a38e994cefc72507c8fb69af58f63f921 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 20 Mar 2022 21:50:30 +0100 Subject: [PATCH 114/605] fix: retranslate CustomCommands --- .../ui/pages/global/CustomCommandsPage.cpp | 3 +- .../pages/instance/InstanceSettingsPage.cpp | 2 + launcher/ui/widgets/CustomCommands.cpp | 39 ++++++++++++++++++ launcher/ui/widgets/CustomCommands.h | 41 ++++++++++++++----- 4 files changed, 74 insertions(+), 11 deletions(-) diff --git a/launcher/ui/pages/global/CustomCommandsPage.cpp b/launcher/ui/pages/global/CustomCommandsPage.cpp index 65544d5c..436d766e 100644 --- a/launcher/ui/pages/global/CustomCommandsPage.cpp +++ b/launcher/ui/pages/global/CustomCommandsPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -87,5 +88,5 @@ void CustomCommandsPage::loadSettings() void CustomCommandsPage::retranslate() { - // fixme: implement + commands->retranslate(); } diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index fc8a2cff..e68a7124 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -378,4 +379,5 @@ void InstanceSettingsPage::checkerFinished() void InstanceSettingsPage::retranslate() { ui->retranslateUi(this); + ui->customCommands->retranslate(); // TODO: why is this seperate from the others? } diff --git a/launcher/ui/widgets/CustomCommands.cpp b/launcher/ui/widgets/CustomCommands.cpp index 24bdc07d..5a718b54 100644 --- a/launcher/ui/widgets/CustomCommands.cpp +++ b/launcher/ui/widgets/CustomCommands.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "CustomCommands.h" #include "ui_CustomCommands.h" @@ -26,6 +61,10 @@ void CustomCommands::initialize(bool checkable, bool checked, const QString& pre } +void CustomCommands::retranslate() { + ui->retranslateUi(this); +} + bool CustomCommands::checked() const { if(!ui->customCommandsGroupBox->isCheckable()) diff --git a/launcher/ui/widgets/CustomCommands.h b/launcher/ui/widgets/CustomCommands.h index 8db991fa..4a7a17ef 100644 --- a/launcher/ui/widgets/CustomCommands.h +++ b/launcher/ui/widgets/CustomCommands.h @@ -1,16 +1,36 @@ -/* Copyright 2018-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -31,6 +51,7 @@ public: virtual ~CustomCommands(); void initialize(bool checkable, bool checked, const QString & prelaunch, const QString & wrapper, const QString & postexit); + void retranslate(); bool checked() const; QString prelaunchCommand() const; QString wrapperCommand() const; From 26acc836d9c9a0be5363c306f825f33602298c5d Mon Sep 17 00:00:00 2001 From: Philipp David Date: Mon, 21 Mar 2022 09:40:20 +0100 Subject: [PATCH 115/605] Revert "fix: use our own prefix for rainbow lib" This reverts commit 61db1c46beb465c33124ec4f34dfdcefd4d804d3. --- launcher/CMakeLists.txt | 2 +- libraries/rainbow/CMakeLists.txt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6491ccce..ab031a00 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -941,7 +941,7 @@ target_link_libraries(Launcher_logic Launcher_iconfix QuaZip::QuaZip hoedown - PolyMC_rainbow + Launcher_rainbow LocalPeer ) diff --git a/libraries/rainbow/CMakeLists.txt b/libraries/rainbow/CMakeLists.txt index a07135c3..833538e3 100644 --- a/libraries/rainbow/CMakeLists.txt +++ b/libraries/rainbow/CMakeLists.txt @@ -9,14 +9,14 @@ src/rainbow.cpp ) add_definitions(-DRAINBOW_LIBRARY) -add_library(PolyMC_rainbow SHARED ${RAINBOW_SOURCES}) -target_include_directories(PolyMC_rainbow PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") +add_library(Launcher_rainbow SHARED ${RAINBOW_SOURCES}) +target_include_directories(Launcher_rainbow PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(PolyMC_rainbow Qt5::Core Qt5::Gui) +target_link_libraries(Launcher_rainbow Qt5::Core Qt5::Gui) # Install it install( - TARGETS PolyMC_rainbow + TARGETS Launcher_rainbow RUNTIME DESTINATION ${LIBRARY_DEST_DIR} LIBRARY DESTINATION ${LIBRARY_DEST_DIR} ) From e8373bbf65c62096588a19c09959f1212ae14403 Mon Sep 17 00:00:00 2001 From: Philipp David Date: Fri, 18 Mar 2022 10:28:35 +0100 Subject: [PATCH 116/605] Build with static rainbow --- launcher/CMakeLists.txt | 2 +- libraries/rainbow/CMakeLists.txt | 10 +-------- libraries/rainbow/include/rainbow.h | 20 ++++++++--------- libraries/rainbow/include/rainbow_config.h | 26 ---------------------- 4 files changed, 11 insertions(+), 47 deletions(-) delete mode 100644 libraries/rainbow/include/rainbow_config.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index ab031a00..98cb0a3b 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -941,8 +941,8 @@ target_link_libraries(Launcher_logic Launcher_iconfix QuaZip::QuaZip hoedown - Launcher_rainbow LocalPeer + Launcher_rainbow ) target_link_libraries(Launcher_logic) diff --git a/libraries/rainbow/CMakeLists.txt b/libraries/rainbow/CMakeLists.txt index 833538e3..e57dbbc2 100644 --- a/libraries/rainbow/CMakeLists.txt +++ b/libraries/rainbow/CMakeLists.txt @@ -8,15 +8,7 @@ set(RAINBOW_SOURCES src/rainbow.cpp ) -add_definitions(-DRAINBOW_LIBRARY) -add_library(Launcher_rainbow SHARED ${RAINBOW_SOURCES}) +add_library(Launcher_rainbow STATIC ${RAINBOW_SOURCES}) target_include_directories(Launcher_rainbow PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") target_link_libraries(Launcher_rainbow Qt5::Core Qt5::Gui) - -# Install it -install( - TARGETS Launcher_rainbow - RUNTIME DESTINATION ${LIBRARY_DEST_DIR} - LIBRARY DESTINATION ${LIBRARY_DEST_DIR} -) diff --git a/libraries/rainbow/include/rainbow.h b/libraries/rainbow/include/rainbow.h index 67c46300..57be87f1 100644 --- a/libraries/rainbow/include/rainbow.h +++ b/libraries/rainbow/include/rainbow.h @@ -23,8 +23,6 @@ #pragma once -#include "rainbow_config.h" - #include class QColor; @@ -43,13 +41,13 @@ namespace Rainbow * * @see http://en.wikipedia.org/wiki/Luma_(video) */ -RAINBOW_EXPORT qreal luma(const QColor &); +qreal luma(const QColor &); /** * Calculate hue, chroma and luma of a color in one call. * @since 5.0 */ -RAINBOW_EXPORT void getHcy(const QColor &, qreal *hue, qreal *chroma, qreal *luma, +void getHcy(const QColor &, qreal *hue, qreal *chroma, qreal *luma, qreal *alpha = 0); /** @@ -64,7 +62,7 @@ RAINBOW_EXPORT void getHcy(const QColor &, qreal *hue, qreal *chroma, qreal *lum * * @see Rainbow::luma */ -RAINBOW_EXPORT qreal contrastRatio(const QColor &, const QColor &); +qreal contrastRatio(const QColor &, const QColor &); /** * Adjust the luma of a color by changing its distance from white. @@ -81,7 +79,7 @@ RAINBOW_EXPORT qreal contrastRatio(const QColor &, const QColor &); * component of the color; 1.0 means no change, 0.0 maximizes chroma * @see Rainbow::shade */ -RAINBOW_EXPORT QColor +QColor lighten(const QColor &, qreal amount = 0.5, qreal chromaInverseGain = 1.0); /** @@ -99,7 +97,7 @@ lighten(const QColor &, qreal amount = 0.5, qreal chromaInverseGain = 1.0); * component of the color; 1.0 means no change, 0.0 minimizes chroma * @see Rainbow::shade */ -RAINBOW_EXPORT QColor darken(const QColor &, qreal amount = 0.5, qreal chromaGain = 1.0); +QColor darken(const QColor &, qreal amount = 0.5, qreal chromaGain = 1.0); /** * Adjust the luma and chroma components of a color. The amount is added @@ -113,7 +111,7 @@ RAINBOW_EXPORT QColor darken(const QColor &, qreal amount = 0.5, qreal chromaGai * 1.0 maximizes chroma * @see Rainbow::luma */ -RAINBOW_EXPORT QColor shade(const QColor &, qreal lumaAmount, qreal chromaAmount = 0.0); +QColor shade(const QColor &, qreal lumaAmount, qreal chromaAmount = 0.0); /** * Create a new color by tinting one color with another. This function is @@ -127,7 +125,7 @@ RAINBOW_EXPORT QColor shade(const QColor &, qreal lumaAmount, qreal chromaAmount * @param amount how strongly to tint the base; 0.0 gives @p base, * 1.0 gives @p color */ -RAINBOW_EXPORT QColor tint(const QColor &base, const QColor &color, qreal amount = 0.3); +QColor tint(const QColor &base, const QColor &color, qreal amount = 0.3); /** * Blend two colors into a new color by linear combination. @@ -140,7 +138,7 @@ RAINBOW_EXPORT QColor tint(const QColor &base, const QColor &color, qreal amount * @p bias >= 1 gives @p c2. @p bias == 0.5 gives a 50% blend of @p c1 * and @p c2. */ -RAINBOW_EXPORT QColor mix(const QColor &c1, const QColor &c2, qreal bias = 0.5); +QColor mix(const QColor &c1, const QColor &c2, qreal bias = 0.5); /** * Blend two colors into a new color by painting the second color over the @@ -154,7 +152,7 @@ RAINBOW_EXPORT QColor mix(const QColor &c1, const QColor &c2, qreal bias = 0.5); * @param paint the color to be overlayed onto the base color. * @param comp the CompositionMode used to do the blending. */ -RAINBOW_EXPORT QColor +QColor overlayColors(const QColor &base, const QColor &paint, QPainter::CompositionMode comp = QPainter::CompositionMode_SourceOver); } diff --git a/libraries/rainbow/include/rainbow_config.h b/libraries/rainbow/include/rainbow_config.h deleted file mode 100644 index 52cc7388..00000000 --- a/libraries/rainbow/include/rainbow_config.h +++ /dev/null @@ -1,26 +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 - -#ifdef RAINBOW_STATIC - #define RAINBOW_EXPORT -#else - #ifdef RAINBOW_LIBRARY - #define RAINBOW_EXPORT Q_DECL_EXPORT - #else - #define RAINBOW_EXPORT Q_DECL_IMPORT - #endif -#endif \ No newline at end of file From 3c0c57359b0a85313f962582b56f6e561366dfd0 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 21 Mar 2022 14:46:01 +0100 Subject: [PATCH 117/605] feat(actions): add backport bot --- .github/workflows/backport.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/backport.yml diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 00000000..e1a7cffa --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,17 @@ +name: Backport PR to stable +on: + pull_request: + branches: [ develop ] + types: [ closed ] +jobs: + release_pull_request: + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'backport') + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v1 + - name: Backport PR by cherry-pick-ing + uses: Nathanmalnoury/gh-backport-action@master + with: + pr_branch: 'stable' + github_token: ${{ secrets.GITHUB_TOKEN }} From 2741c58a01bbc0c8d1f8768299cfcf0ebf3fb275 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Mon, 21 Mar 2022 19:11:55 +0100 Subject: [PATCH 118/605] bring back notportable builds --- .github/workflows/build.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac181079..8819b80b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,10 +27,23 @@ jobs: - os: windows-2022 name: "Windows-i686" msystem: mingw32 + portable: false - os: windows-2022 name: "Windows-x86_64" msystem: mingw64 + portable: false + + - os: windows-2022 + name: "Windows-i686-portable" + msystem: mingw32 + portable: true + + - os: windows-2022 + name: "Windows-x86_64-portable" + msystem: mingw64 + portable: true + - os: macos-11 qt_version: 5.12.12 @@ -138,11 +151,17 @@ jobs: run: | cmake --build ${{ env.BUILD_DIR }} - - name: Build on Windows - if: runner.os == 'Windows' + - name: Build on Windows portable + if: runner.os == 'Windows' && matrix.portable == true shell: msys2 {0} run: | cmake --build ${{ env.BUILD_DIR }} + + - name: Build on Windows + if: runner.os == 'Windows' && matrix.portable != true + shell: msys2 {0} + run: | + cmake --build ${{ env.BUILD_DIR }} _DLauncher_PORTABLE=OFF - name: Install if: runner.os != 'Linux' && runner.os != 'Windows' From d1d055564ccdcd7bb4aa37cb4cd42b7429cffa96 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Mon, 21 Mar 2022 19:21:09 +0100 Subject: [PATCH 119/605] fix typo --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8819b80b..b0a57c41 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -161,7 +161,7 @@ jobs: if: runner.os == 'Windows' && matrix.portable != true shell: msys2 {0} run: | - cmake --build ${{ env.BUILD_DIR }} _DLauncher_PORTABLE=OFF + cmake --build ${{ env.BUILD_DIR }} -DLauncher_PORTABLE=OFF - name: Install if: runner.os != 'Linux' && runner.os != 'Windows' From 8accb6f04e1b9bcbfaf68f7930ffd12fddebccf8 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Mon, 21 Mar 2022 19:29:33 +0100 Subject: [PATCH 120/605] fix typos opz :P --- .github/workflows/build.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0a57c41..5fb98faa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -135,11 +135,18 @@ jobs: run: | cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja - - name: Configure CMake on Windows - if: runner.os == 'Windows' + - name: Configure CMake on Windows + if: runner.os == 'Windows' && matrix.portable != true shell: msys2 {0} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja -DLauncher_PORTABLE=OFF + + + - name: Configure CMake on Windows portable + if: runner.os == 'Windows' && matrix.portable == true + shell: msys2 {0} + run: | + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja - name: Configure CMake on Linux if: runner.os == 'Linux' @@ -151,17 +158,11 @@ jobs: run: | cmake --build ${{ env.BUILD_DIR }} - - name: Build on Windows portable - if: runner.os == 'Windows' && matrix.portable == true + - name: Build on Windows + if: runner.os == 'Windows' shell: msys2 {0} run: | cmake --build ${{ env.BUILD_DIR }} - - - name: Build on Windows - if: runner.os == 'Windows' && matrix.portable != true - shell: msys2 {0} - run: | - cmake --build ${{ env.BUILD_DIR }} -DLauncher_PORTABLE=OFF - name: Install if: runner.os != 'Linux' && runner.os != 'Windows' From c6b1a776dcfada3408f4265a22fdbfdd23deccd9 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Tue, 22 Mar 2022 07:38:00 +0100 Subject: [PATCH 121/605] fix some typos --- .github/workflows/build.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5fb98faa..4aa316f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,6 @@ jobs: msystem: mingw64 portable: true - - os: macos-11 qt_version: 5.12.12 qt_host: mac @@ -139,7 +138,7 @@ jobs: if: runner.os == 'Windows' && matrix.portable != true shell: msys2 {0} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja -DLauncher_PORTABLE=OFF + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_PORTABLE=OFF -G Ninja - name: Configure CMake on Windows portable @@ -158,7 +157,7 @@ jobs: run: | cmake --build ${{ env.BUILD_DIR }} - - name: Build on Windows + - name: Build on Windows if: runner.os == 'Windows' shell: msys2 {0} run: | From 0681568d3e411b6fa08698fc74d7042db438099d Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Sun, 13 Mar 2022 13:22:42 +1100 Subject: [PATCH 122/605] Merge pull request #254 from Scrumplex/fix-update-metainfo Update metainfo URLs --- program_info/org.polymc.PolyMC.metainfo.xml.in | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/program_info/org.polymc.PolyMC.metainfo.xml.in b/program_info/org.polymc.PolyMC.metainfo.xml.in index 03401f3d..7972965c 100644 --- a/program_info/org.polymc.PolyMC.metainfo.xml.in +++ b/program_info/org.polymc.PolyMC.metainfo.xml.in @@ -11,7 +11,7 @@ CC0-1.0 GPL-3.0-or-later https://polymc.org/ - https://github.com/PolyMC/PolyMC#help--support + https://polymc.org/wiki/

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

Features:

@@ -28,23 +28,23 @@ The main PolyMC window - https://polymc.github.io/assets/img/screenshots/LauncherDark.png + https://polymc.org/img/screenshots/LauncherDark.png Modpack installation - https://polymc.github.io/assets/img/screenshots/ModpackInstallDark.png + https://polymc.org/img/screenshots/ModpackInstallDark.png Mod installation - https://polymc.github.io/assets/img/screenshots/ModInstallDark.png + https://polymc.org/img/screenshots/ModInstallDark.png Instance management - https://polymc.github.io/assets/img/screenshots/PropertiesDark.png + https://polymc.org/img/screenshots/PropertiesDark.png Cat :) - https://polymc.github.io/assets/img/screenshots/LauncherCatDark.png + https://polymc.org/img/screenshots/LauncherCatDark.png From c560d06b8df9e4fb6900246dc7d2cb6aeec046c1 Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Tue, 15 Mar 2022 00:50:49 +1100 Subject: [PATCH 123/605] Merge pull request #270 from flowln/dialog Make a better "Mod download confirmation dialog" --- launcher/CMakeLists.txt | 3 + launcher/ui/dialogs/ModDownloadDialog.cpp | 27 ++----- launcher/ui/dialogs/ReviewMessageBox.cpp | 31 +++++++ launcher/ui/dialogs/ReviewMessageBox.h | 23 ++++++ launcher/ui/dialogs/ReviewMessageBox.ui | 98 +++++++++++++++++++++++ 5 files changed, 163 insertions(+), 19 deletions(-) create mode 100644 launcher/ui/dialogs/ReviewMessageBox.cpp create mode 100644 launcher/ui/dialogs/ReviewMessageBox.h create mode 100644 launcher/ui/dialogs/ReviewMessageBox.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 86c05651..039841ef 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -797,6 +797,8 @@ SET(LAUNCHER_SOURCES ui/pagedialog/PageDialog.h ui/dialogs/ProgressDialog.cpp ui/dialogs/ProgressDialog.h + ui/dialogs/ReviewMessageBox.cpp + ui/dialogs/ReviewMessageBox.h ui/dialogs/UpdateDialog.cpp ui/dialogs/UpdateDialog.h ui/dialogs/VersionSelectDialog.cpp @@ -905,6 +907,7 @@ qt5_wrap_ui(LAUNCHER_UI ui/dialogs/AboutDialog.ui ui/dialogs/LoginDialog.ui ui/dialogs/EditAccountDialog.ui + ui/dialogs/ReviewMessageBox.ui ) qt5_add_resources(LAUNCHER_RESOURCES diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 23ca8731..a53f93e8 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -5,7 +5,7 @@ #include #include "ProgressDialog.h" -#include "CustomMessageBox.h" +#include "ReviewMessageBox.h" #include #include @@ -75,27 +75,16 @@ void ModDownloadDialog::confirm() auto keys = modTask.keys(); keys.sort(Qt::CaseInsensitive); - auto info = QString(tr("You're about to download the following mods:")); - info.append("\n\n"); - for(auto task : keys){ - info.append(task); - info.append("\n --> "); - info.append(tr("File name: ")); - info.append(modTask.find(task).value()->getFilename()); - info.append('\n'); - } - - auto confirm_dialog = CustomMessageBox::selectable( + auto confirm_dialog = ReviewMessageBox::create( this, - tr("Confirm mods to download"), - info, - QMessageBox::NoIcon, - QMessageBox::Cancel | QMessageBox::Ok, - QMessageBox::Ok + tr("Confirm mods to download") ); - auto AcceptButton = confirm_dialog->button(QMessageBox::Ok); - connect(AcceptButton, &QPushButton::clicked, this, &ModDownloadDialog::accept); + for(auto task : keys){ + confirm_dialog->appendMod(task, modTask.find(task).value()->getFilename()); + } + + connect(confirm_dialog, &QDialog::accepted, this, &ModDownloadDialog::accept); confirm_dialog->open(); } diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp new file mode 100644 index 00000000..2bfd02e0 --- /dev/null +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -0,0 +1,31 @@ +#include "ReviewMessageBox.h" +#include "ui_ReviewMessageBox.h" + +ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QString const& icon) + : QDialog(parent), ui(new Ui::ReviewMessageBox) +{ + ui->setupUi(this); +} + +ReviewMessageBox::~ReviewMessageBox() +{ + delete ui; +} + +auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon) -> ReviewMessageBox* +{ + return new ReviewMessageBox(parent, title, icon); +} + +void ReviewMessageBox::appendMod(const QString& name, const QString& filename) +{ + auto itemTop = new QTreeWidgetItem(ui->modTreeWidget); + itemTop->setText(0, name); + + auto filenameItem = new QTreeWidgetItem(itemTop); + filenameItem->setText(0, tr("Filename: %1").arg(filename)); + + itemTop->insertChildren(0, { filenameItem }); + + ui->modTreeWidget->addTopLevelItem(itemTop); +} diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h new file mode 100644 index 00000000..48742cd9 --- /dev/null +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace Ui { +class ReviewMessageBox; +} + +class ReviewMessageBox final : public QDialog { + Q_OBJECT + + public: + static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*; + + void appendMod(const QString& name, const QString& filename); + + ~ReviewMessageBox(); + + private: + ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon); + + Ui::ReviewMessageBox* ui; +}; diff --git a/launcher/ui/dialogs/ReviewMessageBox.ui b/launcher/ui/dialogs/ReviewMessageBox.ui new file mode 100644 index 00000000..d04f3b3f --- /dev/null +++ b/launcher/ui/dialogs/ReviewMessageBox.ui @@ -0,0 +1,98 @@ + + + ReviewMessageBox + + + + 0 + 0 + 400 + 300 + + + + Confirm mod selection + + + true + + + true + + + + + + You're about to download the following mods: + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + true + + + QAbstractItemView::NoSelection + + + QAbstractItemView::SelectItems + + + false + + + + + + + + + + + + + + buttonBox + accepted() + ReviewMessageBox + accept() + + + 200 + 265 + + + 199 + 149 + + + + + buttonBox + rejected() + ReviewMessageBox + reject() + + + 200 + 265 + + + 199 + 149 + + + + + From 76e1aba58b71fff9293f2cff6ba581629f6f54bc Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 14 Mar 2022 23:31:23 +0100 Subject: [PATCH 124/605] Merge pull request #273 from DioEgizio/extra-rebranding-macos extra rebranding for macos --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5250d0ff..5b1b97d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -170,13 +170,13 @@ if(UNIX AND APPLE) # Mac bundle settings set(MACOSX_BUNDLE_BUNDLE_NAME "${Launcher_Name}") - set(MACOSX_BUNDLE_INFO_STRING "${Launcher_Name}: Minecraft launcher and management utility.") + set(MACOSX_BUNDLE_INFO_STRING "${Launcher_Name}: A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.") set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.polymc.${Launcher_Name}") set(MACOSX_BUNDLE_BUNDLE_VERSION "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}") set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}") set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}") set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns) - set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2015-2021 ${Launcher_Copyright}") + set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2021-2022 ${Launcher_Copyright}") # directories to look for dependencies set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) From d16c6b0e691061f404eb5bab2004504918db4de3 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 14 Mar 2022 23:31:38 +0100 Subject: [PATCH 125/605] Merge pull request #265 from Scrumplex/fix-javacheck-appimage Define JARs path relative to application root --- CMakeLists.txt | 3 ++- launcher/Application.cpp | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b1b97d7..c087e95f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -207,7 +207,8 @@ elseif(UNIX) set(LAUNCHER_METAINFO_DEST_DIR "share/metainfo" CACHE STRING "Path to the metainfo directory") set(LAUNCHER_ICON_DEST_DIR "share/icons/hicolor/scalable/apps" CACHE STRING "Path to the scalable icon directory") - set(Launcher_APP_BINARY_DEFS "-DMULTIMC_JARS_LOCATION=${CMAKE_INSTALL_PREFIX}/${JARS_DEST_DIR}") + # jars path is determined on runtime, relative to "Application root path", generally /usr for Launcher_PORTABLE=0 + set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_JARS_LOCATION=${JARS_DEST_DIR}") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${LAUNCHER_DESKTOP_DEST_DIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${LAUNCHER_METAINFO_DEST_DIR}) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 0ce80d4b..c699d840 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -515,8 +515,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) FS::updateTimestamp(m_rootPath); #endif -#ifdef MULTIMC_JARS_LOCATION - m_jarsPath = TOSTRING(MULTIMC_JARS_LOCATION); +#ifdef LAUNCHER_JARS_LOCATION + m_jarsPath = TOSTRING(LAUNCHER_JARS_LOCATION); #endif qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT; @@ -1494,7 +1494,7 @@ QString Application::getJarsPath() { return FS::PathCombine(QCoreApplication::applicationDirPath(), "jars"); } - return m_jarsPath; + return FS::PathCombine(m_rootPath, m_jarsPath); } QString Application::getMSAClientID() From 4cf3ac42c84d3be00ed923286c7e1f8adda886f6 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 16 Mar 2022 07:27:29 +0100 Subject: [PATCH 126/605] Merge pull request #280 from oynqr/gitdir-notfound-check Add GITDIR-NOTFOUND check --- buildconfig/BuildConfig.cpp.in | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index d1bd4d05..5c374508 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -30,7 +30,11 @@ Config::Config() GIT_COMMIT = "@Launcher_GIT_COMMIT@"; GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; - if(GIT_REFSPEC.startsWith("refs/heads/")) + if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND")) + { + VERSION_CHANNEL = QStringLiteral("stable"); + } + else if(GIT_REFSPEC.startsWith("refs/heads/")) { VERSION_CHANNEL = GIT_REFSPEC; VERSION_CHANNEL.remove("refs/heads/"); From 149ffb844ff4302b0631cfe34bccaae7c5722f67 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 16 Mar 2022 12:37:05 +0100 Subject: [PATCH 127/605] Merge pull request #278 from Scrumplex/fix-copying Clarify GPL-3.0-only --- COPYING.md | 3 +-- program_info/org.polymc.PolyMC.metainfo.xml.in | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/COPYING.md b/COPYING.md index 1ac6d5cb..273a5b3a 100644 --- a/COPYING.md +++ b/COPYING.md @@ -5,8 +5,7 @@ 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, either version 3 of the License, or - (at your option) any later version. + 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 diff --git a/program_info/org.polymc.PolyMC.metainfo.xml.in b/program_info/org.polymc.PolyMC.metainfo.xml.in index 7972965c..ff4af1c3 100644 --- a/program_info/org.polymc.PolyMC.metainfo.xml.in +++ b/program_info/org.polymc.PolyMC.metainfo.xml.in @@ -9,7 +9,7 @@ PolyMC Team A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once CC0-1.0 - GPL-3.0-or-later + GPL-3.0-only https://polymc.org/ https://polymc.org/wiki/ From 7471ce4530f3306805dce8ccad7d90fb80396c2b Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Sat, 19 Mar 2022 11:39:48 +1100 Subject: [PATCH 128/605] Merge pull request #299 from flowln/paste_serv Remove paste.polymc.org --- launcher/ui/pages/global/APIPage.ui | 5 ----- 1 file changed, 5 deletions(-) diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 28c53b79..1bc41e5a 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -74,11 +74,6 @@ https://0x0.st
- - - https://paste.polymc.org - -
From 6d9eaee7f96f1ca255bf950323003af5d718aa5a Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Sat, 19 Mar 2022 11:40:17 +1100 Subject: [PATCH 129/605] Merge pull request #297 from DioEgizio/patch-1 bundle jre8u312 instead of latest on appimage --- .github/scripts/prepare_JREs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/prepare_JREs.sh b/.github/scripts/prepare_JREs.sh index b85e9c2f..ee713f81 100755 --- a/.github/scripts/prepare_JREs.sh +++ b/.github/scripts/prepare_JREs.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -URL_JDK8="https://api.adoptium.net/v3/binary/latest/8/ga/linux/x64/jre/hotspot/normal/eclipse" +URL_JDK8="https://api.adoptium.net/v3/binary/version/jdk8u312-b07/linux/x64/jre/hotspot/normal/eclipse" URL_JDK17="https://api.adoptium.net/v3/binary/latest/17/ga/linux/x64/jre/hotspot/normal/eclipse" mkdir -p JREs From 025a3cf730c5a523b1966d2a20cdfbfbc4542fd7 Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Sat, 19 Mar 2022 11:41:36 +1100 Subject: [PATCH 130/605] Merge pull request #296 from flowln/right_file Use primary file for mod downloading on Modrinth --- .../modrinth/ModrinthPackIndex.cpp | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 9017eb67..9581ca04 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -51,31 +51,32 @@ void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray auto files = Json::requireArray(obj, "files"); int i = 0; - while (files.count() > 1 && i < files.count()){ - //try to resolve the correct file + + // Find correct file (needed in cases where one version may have multiple files) + // Will default to the last one if there's no primary (though I think Modrinth requires that + // at least one file is primary, idk) + while (i < files.count()){ auto parent = files[i].toObject(); auto fileName = Json::requireString(parent, "filename"); - //avoid grabbing "dev" files - if(fileName.contains("javadocs",Qt::CaseInsensitive) || fileName.contains("sources",Qt::CaseInsensitive)){ + + // Grab the correct mod loader + if(hasFabric){ + if(fileName.contains("forge",Qt::CaseInsensitive)){ + i++; + continue; + } + } else if(fileName.contains("fabric", Qt::CaseInsensitive)){ i++; continue; } - //grab the correct mod loader - if(fileName.contains("forge",Qt::CaseInsensitive) || fileName.contains("fabric",Qt::CaseInsensitive) ){ - if(hasFabric){ - if(fileName.contains("forge",Qt::CaseInsensitive)){ - i++; - continue; - } - }else{ - if(fileName.contains("fabric",Qt::CaseInsensitive)){ - i++; - continue; - } - } - } - break; + + // Grab the primary file, if available + if(Json::requireBoolean(parent, "primary")) + break; + + i++; } + auto parent = files[i].toObject(); if(parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); From bf6fa6bce404279332dcf157fa1025b10d2ea591 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 19 Mar 2022 15:59:38 +0100 Subject: [PATCH 131/605] Merge pull request #295 from Scrumplex/fix-portable Make Launcher_PORTABLE work on all platforms --- CMakeLists.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c087e95f..12ff771b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -155,6 +155,10 @@ add_subdirectory(program_info) # Install the build results according to platform set(Launcher_PORTABLE 1 CACHE BOOL "The type of installation (Portable or System)") +if (Launcher_PORTABLE) + # launcher/Application.cpp will use this value + set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_PORTABLE") +endif() if(UNIX AND APPLE) set(BINARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") @@ -194,9 +198,6 @@ elseif(UNIX) set(BUNDLE_DEST_DIR ".") set(JARS_DEST_DIR "bin/jars") - # launcher/Application.cpp will use this value - set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_PORTABLE") - # Install basic runner script configure_file(launcher/Launcher.in "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" @ONLY) install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" DESTINATION ${BUNDLE_DEST_DIR} RENAME ${Launcher_Name}) From ddda02f092450f3d5f3617eaabc25fa9b62d9747 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 19 Mar 2022 16:02:28 +0100 Subject: [PATCH 132/605] Merge pull request #292 from lack/offline_username_limits Limit offline username to 16 characters with override --- launcher/ui/dialogs/OfflineLoginDialog.cpp | 9 +++++++++ launcher/ui/dialogs/OfflineLoginDialog.h | 1 + launcher/ui/dialogs/OfflineLoginDialog.ui | 13 +++++++++++++ 3 files changed, 23 insertions(+) diff --git a/launcher/ui/dialogs/OfflineLoginDialog.cpp b/launcher/ui/dialogs/OfflineLoginDialog.cpp index 345ed40a..4f3d8be4 100644 --- a/launcher/ui/dialogs/OfflineLoginDialog.cpp +++ b/launcher/ui/dialogs/OfflineLoginDialog.cpp @@ -42,6 +42,15 @@ void OfflineLoginDialog::setUserInputsEnabled(bool 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) { diff --git a/launcher/ui/dialogs/OfflineLoginDialog.h b/launcher/ui/dialogs/OfflineLoginDialog.h index 5e608379..fdb3d91f 100644 --- a/launcher/ui/dialogs/OfflineLoginDialog.h +++ b/launcher/ui/dialogs/OfflineLoginDialog.h @@ -35,6 +35,7 @@ slots: void onTaskProgress(qint64 current, qint64 total); void on_userTextBox_textEdited(const QString &newText); + void on_allowLongUsernames_stateChanged(int value); private: Ui::OfflineLoginDialog *ui; diff --git a/launcher/ui/dialogs/OfflineLoginDialog.ui b/launcher/ui/dialogs/OfflineLoginDialog.ui index d8964a2e..4633cbe3 100644 --- a/launcher/ui/dialogs/OfflineLoginDialog.ui +++ b/launcher/ui/dialogs/OfflineLoginDialog.ui @@ -35,11 +35,24 @@ + + 16 + Username + + + + Usernames longer than 16 characters cannot be used for LAN games or offline-mode servers. + + + Allow long usernames + + + From 571e322d66de8eaccdb634fce7f553848f9b4f82 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 20 Mar 2022 12:04:39 +0100 Subject: [PATCH 133/605] Merge pull request #200 from Scrumplex/scrumplex-license-header --- buildconfig/BuildConfig.cpp.in | 35 ++++++++++++++++ buildconfig/BuildConfig.h | 35 ++++++++++++++++ launcher/Application.cpp | 35 ++++++++++++++++ launcher/Application.h | 35 ++++++++++++++++ launcher/BaseInstance.cpp | 40 ++++++++++++++----- launcher/BaseInstance.h | 40 ++++++++++++++----- launcher/LaunchController.cpp | 35 ++++++++++++++++ launcher/LaunchController.h | 35 ++++++++++++++++ launcher/MMCZip.cpp | 40 ++++++++++++++----- launcher/MMCZip.h | 40 ++++++++++++++----- launcher/minecraft/auth/AccountData.cpp | 35 ++++++++++++++++ launcher/minecraft/auth/AccountData.h | 35 ++++++++++++++++ launcher/minecraft/auth/AccountList.cpp | 40 ++++++++++++++----- launcher/minecraft/auth/AccountList.h | 40 ++++++++++++++----- launcher/minecraft/auth/AccountTask.cpp | 40 ++++++++++++++----- launcher/minecraft/auth/AccountTask.h | 40 ++++++++++++++----- launcher/minecraft/auth/MinecraftAccount.cpp | 42 +++++++++++++++----- launcher/minecraft/auth/MinecraftAccount.h | 40 ++++++++++++++----- launcher/minecraft/auth/steps/MSAStep.cpp | 35 ++++++++++++++++ launcher/minecraft/auth/steps/MSAStep.h | 34 ++++++++++++++++ launcher/ui/dialogs/ExportInstanceDialog.cpp | 40 ++++++++++++++----- launcher/ui/dialogs/MSALoginDialog.cpp | 40 ++++++++++++++----- launcher/ui/pages/global/APIPage.cpp | 40 ++++++++++++++----- launcher/ui/pages/global/APIPage.h | 40 ++++++++++++++----- launcher/ui/pages/global/AccountListPage.cpp | 40 ++++++++++++++----- launcher/ui/pages/global/AccountListPage.h | 40 ++++++++++++++----- 26 files changed, 830 insertions(+), 161 deletions(-) diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 5c374508..8df627b0 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "BuildConfig.h" #include diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index cf74b39e..121cfc8f 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 diff --git a/launcher/Application.cpp b/launcher/Application.cpp index c699d840..cbcacb5e 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "Application.h" #include "BuildConfig.h" diff --git a/launcher/Application.h b/launcher/Application.h index fb41d647..c3e29ef5 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 1bff9e1d..2fb31d94 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "BaseInstance.h" diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 488f2781..c973fcd4 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 4fd2eade..792d8381 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "LaunchController.h" #include "minecraft/auth/AccountList.h" #include "Application.h" diff --git a/launcher/LaunchController.h b/launcher/LaunchController.h index 7ed4b09e..2171ad5e 100644 --- a/launcher/LaunchController.h +++ b/launcher/LaunchController.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 9d7e4cc2..b92f1781 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index 0f7aa254..bf90cd0b 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index f791db14..dd9c3f8f 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "AccountData.h" #include #include diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 6749a471..092e1691 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index e404cdda..3422df7c 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "AccountList.h" diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index 025926ae..baaf7414 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp index 321b350f..49b6e46f 100644 --- a/launcher/minecraft/auth/AccountTask.cpp +++ b/launcher/minecraft/auth/AccountTask.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "AccountTask.h" diff --git a/launcher/minecraft/auth/AccountTask.h b/launcher/minecraft/auth/AccountTask.h index c2a5d86c..1bd6c59f 100644 --- a/launcher/minecraft/auth/AccountTask.h +++ b/launcher/minecraft/auth/AccountTask.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index a604cadf..ec86fa5c 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -1,18 +1,38 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * Authors: Orochimarufan + * 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. * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . * - * 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. + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Authors: Orochimarufan + * + * 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 "MinecraftAccount.h" diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 6592f9c0..7777f846 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index 207d9373..16afcb42 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "MSAStep.h" #include diff --git a/launcher/minecraft/auth/steps/MSAStep.h b/launcher/minecraft/auth/steps/MSAStep.h index 301e1465..e9a1524e 100644 --- a/launcher/minecraft/auth/steps/MSAStep.h +++ b/launcher/minecraft/auth/steps/MSAStep.h @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index f3bf7abe..5fac1015 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "ExportInstanceDialog.h" diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index 174ad46c..b11b6980 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "MSALoginDialog.h" diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index ad79e00c..037ec217 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC & PolyMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "APIPage.h" diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index 9474ebbb..d9a84753 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index eb1ee8d3..af1d9d60 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "AccountListPage.h" diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index 841c3fd2..c1dea8be 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 From 8b46658b05b3c85aab24183931583fbaaea6a228 Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Mon, 21 Mar 2022 01:01:05 +1100 Subject: [PATCH 134/605] Merge pull request #294 from oynqr/msys2 Switch to msys2 for Windows builds --- .github/workflows/build.yml | 94 +++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b5797e95..ac181079 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,9 +25,12 @@ jobs: app_image: true - os: windows-2022 - qt_version: 5.15.2 - qt_host: windows - qt_arch: win32_mingw81 + name: "Windows-i686" + msystem: mingw32 + + - os: windows-2022 + name: "Windows-x86_64" + msystem: mingw64 - os: macos-11 qt_version: 5.12.12 @@ -42,32 +45,24 @@ jobs: BUILD_DIR: "build" steps: - - name: Install 32bit mingw on Windows - if: runner.os == 'Windows' - uses: egor-tensin/setup-mingw@v2 - with: - platform: x86 - - - name: Install 32bit zlib via Strawberry on Windows - if: runner.os == 'Windows' - run: | - choco install strawberryperl -y --force --x86 - - name: Checkout uses: actions/checkout@v2 with: submodules: 'true' - # We need to do this here because it inexplicably fails if we split the step - - name: Download and install OpenSSL libs on Windows + - name: 'Setup MSYS2' if: runner.os == 'Windows' - run: | - python -m pip install --upgrade pip - python -m pip install aqtinstall==2.0.5 - python -m aqt install-tool -O "${{ github.workspace }}\Qt\" windows desktop tools_openssl_x86 - mkdir ${{ env.INSTALL_DIR }} - copy "${{ github.workspace }}\Qt\Tools\OpenSSL\Win_x86\bin\libssl-1_1.dll" "${{ github.workspace }}\${{ env.INSTALL_DIR }}\" - copy "${{ github.workspace }}\Qt\Tools\OpenSSL\Win_x86\bin\libcrypto-1_1.dll" "${{ github.workspace }}\${{ env.INSTALL_DIR }}\" + uses: msys2/setup-msys2@v2 + with: + msystem: ${{ matrix.msystem }} + update: true + install: >- + git + pacboy: >- + toolchain:p + cmake:p + ninja:p + qt5:p - name: Set short version shell: bash @@ -76,11 +71,13 @@ jobs: echo "VERSION=$ver_short" >> $GITHUB_ENV - name: Install OpenJDK - uses: AdoptOpenJDK/install-jdk@v1 + uses: actions/setup-java@v3 with: - version: '17' + distribution: 'temurin' + java-version: '17' - name: Cache Qt + if: runner.os != 'Windows' id: cache-qt uses: actions/cache@v2 with: @@ -88,7 +85,7 @@ jobs: key: ${{ runner.os }}-${{ matrix.qt_version }}-${{ matrix.qt_arch }}-qt_cache - name: Install Qt - if: runner.os != 'Linux' || matrix.app_image == true + if: runner.os != 'Linux' && runner.os != 'Windows' || matrix.app_image == true uses: jurplel/install-qt-action@v2 with: version: ${{ matrix.qt_version }} @@ -104,6 +101,7 @@ jobs: sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 - name: Install Ninja + if: runner.os != 'Windows' uses: urkle/action-get-ninja@v1 - name: Download linuxdeploy family for AppImage on Linux @@ -120,7 +118,13 @@ jobs: ${{ github.workspace }}/.github/scripts/prepare_JREs.sh - name: Configure CMake - if: runner.os != 'Linux' + if: runner.os != 'Linux' && runner.os != 'Windows' + run: | + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja + + - name: Configure CMake on Windows + if: runner.os == 'Windows' + shell: msys2 {0} run: | cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja @@ -130,11 +134,24 @@ jobs: cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_PORTABLE=OFF -G Ninja - name: Build + if: runner.os != 'Windows' + run: | + cmake --build ${{ env.BUILD_DIR }} + + - name: Build on Windows + if: runner.os == 'Windows' + shell: msys2 {0} run: | cmake --build ${{ env.BUILD_DIR }} - name: Install - if: runner.os != 'Linux' + if: runner.os != 'Linux' && runner.os != 'Windows' + run: | + cmake --install ${{ env.BUILD_DIR }} + + - name: Install on Windows + if: runner.os == 'Windows' + shell: msys2 {0} run: | cmake --install ${{ env.BUILD_DIR }} @@ -165,11 +182,6 @@ jobs: ./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_DIR }}/usr/share/icons/hicolor/scalable/apps/org.polymc.PolyMC.svg - - name: Run windeployqt - if: runner.os == 'Windows' - run: | - windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw "${{ env.INSTALL_DIR }}/polymc.exe" - - name: Run macdeployqt if: runner.os == 'macOS' run: | @@ -207,11 +219,25 @@ jobs: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage path: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage + - name: Copy OpenSSL libs on Windows x86 + if: runner.os == 'Windows' && matrix.msystem == 'mingw32' + shell: msys2 {0} + run: | + cp /mingw32/bin/libcrypto-1_1.dll ${{ env.INSTALL_DIR }}/ + cp /mingw32/bin/libssl-1_1.dll ${{ env.INSTALL_DIR }}/ + + - name: Copy OpenSSL libs on Windows x86_64 + if: runner.os == 'Windows' && matrix.msystem == 'mingw64' + shell: msys2 {0} + run: | + cp /mingw64/bin/libcrypto-1_1-x64.dll ${{ env.INSTALL_DIR }}/ + cp /mingw64/bin/libssl-1_1-x64.dll ${{ env.INSTALL_DIR }}/ + - name: Upload package for Windows if: runner.os == 'Windows' uses: actions/upload-artifact@v2 with: - name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} + name: PolyMC-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} path: ${{ env.INSTALL_DIR }}/** - name: Upload package for macOS From 58957122b9dd2a19a0504efe8944cfb50e29a012 Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Mon, 21 Mar 2022 01:04:40 +1100 Subject: [PATCH 135/605] Merge pull request #306 from Scrumplex/limit-instance-lengths Limit instance names to 128 chars --- launcher/ui/dialogs/NewInstanceDialog.ui | 6 +++++- launcher/ui/instanceview/InstanceDelegate.cpp | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/NewInstanceDialog.ui b/launcher/ui/dialogs/NewInstanceDialog.ui index 7fb19ff5..8ca0b786 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.ui +++ b/launcher/ui/dialogs/NewInstanceDialog.ui @@ -44,7 +44,11 @@ - + + + 128 + + diff --git a/launcher/ui/instanceview/InstanceDelegate.cpp b/launcher/ui/instanceview/InstanceDelegate.cpp index 3c4ca63f..22ff78cd 100644 --- a/launcher/ui/instanceview/InstanceDelegate.cpp +++ b/launcher/ui/instanceview/InstanceDelegate.cpp @@ -405,6 +405,8 @@ void ListViewDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, QString text = realeditor->toPlainText(); text.replace(QChar('\n'), QChar(' ')); text = text.trimmed(); + // Prevent instance names longer than 128 chars + text.truncate(128); if(text.size() != 0) { model->setData(index, text); From 3db5f3040314f252a64f485f366bd30f793aff86 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 20 Mar 2022 17:46:09 +0100 Subject: [PATCH 136/605] Merge pull request #316 from Scrumplex/fix-disabled-controls --- launcher/ui/pages/instance/ModFolderPage.cpp | 5 +---- launcher/ui/pages/instance/VersionPage.cpp | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index ffb87bbe..c7a0376d 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -244,10 +244,7 @@ void ModFolderPage::on_RunningState_changed(bool running) return; } m_controlsEnabled = !running; - ui->actionAdd->setEnabled(m_controlsEnabled); - ui->actionDisable->setEnabled(m_controlsEnabled); - ui->actionEnable->setEnabled(m_controlsEnabled); - ui->actionRemove->setEnabled(m_controlsEnabled); + ui->actionsToolbar->setEnabled(m_controlsEnabled); } bool ModFolderPage::shouldDisplay() const diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 0fa5f68d..43c449eb 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -212,6 +212,8 @@ void VersionPage::updateVersionControls() // FIXME: this is a dirty hack auto minecraftVersion = Version(m_profile->getComponentVersion("net.minecraft")); + ui->actionInstall_Forge->setEnabled(controlsEnabled); + bool supportsFabric = minecraftVersion >= Version("1.14"); ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric); From cd4851c98be26e05a22ba55cc9804fbdb178d7d3 Mon Sep 17 00:00:00 2001 From: txtsd Date: Sun, 20 Mar 2022 22:20:02 +0530 Subject: [PATCH 137/605] Merge pull request #311 from DioEgizio/patch-2 fix webp --- launcher/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 039841ef..94dffb2f 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -995,7 +995,7 @@ if(INSTALL_BUNDLE STREQUAL "full") DIRECTORY "${QT_PLUGINS_DIR}/imageformats" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime - REGEX "tga|tiff|mng|webp" EXCLUDE + REGEX "tga|tiff|mng" EXCLUDE ) # Icon engines install( @@ -1025,7 +1025,7 @@ if(INSTALL_BUNDLE STREQUAL "full") DIRECTORY "${QT_PLUGINS_DIR}/imageformats" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime - REGEX "tga|tiff|mng|webp" EXCLUDE + REGEX "tga|tiff|mng" EXCLUDE REGEX "d\\." EXCLUDE REGEX "_debug\\." EXCLUDE REGEX "\\.dSYM" EXCLUDE From fcf728f3b5f4923cc05edfeb45f8340f420669cf Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Mon, 21 Mar 2022 11:21:21 +1100 Subject: [PATCH 138/605] Merge pull request #315 from txtsd/display_scaling Allow fractional DPI scaling --- launcher/main.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/launcher/main.cpp b/launcher/main.cpp index 8b572743..275fff32 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -29,6 +29,10 @@ int main(int argc, char *argv[]) QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); +#endif + // initialize Qt Application app(argc, argv); From 0a4a3fece57a68b2df6c5c0da8d990b1f981816c Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Tue, 22 Mar 2022 00:06:16 +1100 Subject: [PATCH 139/605] Merge pull request #323 from Scrumplex/retranslate-pages Retranslate all settings pages --- launcher/ui/pages/BasePage.h | 42 ++++++++++---- launcher/ui/pages/global/APIPage.cpp | 6 ++ launcher/ui/pages/global/APIPage.h | 2 + launcher/ui/pages/global/AccountListPage.cpp | 6 ++ launcher/ui/pages/global/AccountListPage.h | 2 + .../ui/pages/global/CustomCommandsPage.cpp | 41 +++++++++++++ launcher/ui/pages/global/CustomCommandsPage.h | 41 +++++++++---- .../ui/pages/global/ExternalToolsPage.cpp | 45 ++++++++++---- launcher/ui/pages/global/ExternalToolsPage.h | 41 +++++++++---- launcher/ui/pages/global/JavaPage.cpp | 45 ++++++++++---- launcher/ui/pages/global/JavaPage.h | 41 +++++++++---- launcher/ui/pages/global/LanguagePage.cpp | 44 +++++++++++--- launcher/ui/pages/global/LanguagePage.h | 43 ++++++++++---- launcher/ui/pages/global/LauncherPage.cpp | 45 ++++++++++---- launcher/ui/pages/global/LauncherPage.h | 41 +++++++++---- launcher/ui/pages/global/MinecraftPage.cpp | 45 ++++++++++---- launcher/ui/pages/global/MinecraftPage.h | 41 +++++++++---- launcher/ui/pages/global/ProxyPage.cpp | 45 ++++++++++---- launcher/ui/pages/global/ProxyPage.h | 41 +++++++++---- .../ui/pages/instance/GameOptionsPage.cpp | 40 +++++++++++++ launcher/ui/pages/instance/GameOptionsPage.h | 41 +++++++++---- .../pages/instance/InstanceSettingsPage.cpp | 42 ++++++++++++++ .../ui/pages/instance/InstanceSettingsPage.h | 41 +++++++++---- launcher/ui/pages/instance/LogPage.cpp | 40 +++++++++++++ launcher/ui/pages/instance/LogPage.h | 41 +++++++++---- launcher/ui/pages/instance/ModFolderPage.cpp | 45 ++++++++++---- launcher/ui/pages/instance/ModFolderPage.h | 41 +++++++++---- launcher/ui/pages/instance/NotesPage.cpp | 40 +++++++++++++ launcher/ui/pages/instance/NotesPage.h | 41 +++++++++---- launcher/ui/pages/instance/OtherLogsPage.cpp | 45 ++++++++++---- launcher/ui/pages/instance/OtherLogsPage.h | 42 ++++++++++---- launcher/ui/pages/instance/ResourcePackPage.h | 35 +++++++++++ .../ui/pages/instance/ScreenshotsPage.cpp | 40 +++++++++++++ launcher/ui/pages/instance/ScreenshotsPage.h | 41 +++++++++---- launcher/ui/pages/instance/ServersPage.cpp | 40 +++++++++++++ launcher/ui/pages/instance/ServersPage.h | 41 +++++++++---- launcher/ui/pages/instance/ShaderPackPage.h | 35 +++++++++++ launcher/ui/pages/instance/TexturePackPage.h | 35 +++++++++++ launcher/ui/pages/instance/VersionPage.cpp | 45 ++++++++++---- launcher/ui/pages/instance/VersionPage.h | 41 +++++++++---- launcher/ui/pages/instance/WorldListPage.cpp | 45 ++++++++++---- launcher/ui/pages/instance/WorldListPage.h | 41 +++++++++---- launcher/ui/pages/modplatform/ImportPage.cpp | 40 +++++++++++++ launcher/ui/pages/modplatform/ImportPage.h | 41 +++++++++---- launcher/ui/pages/modplatform/VanillaPage.cpp | 40 +++++++++++++ launcher/ui/pages/modplatform/VanillaPage.h | 42 ++++++++++---- .../pages/modplatform/atlauncher/AtlPage.cpp | 46 +++++++++++---- .../ui/pages/modplatform/atlauncher/AtlPage.h | 40 +++++++++---- .../pages/modplatform/flame/FlameModPage.cpp | 40 +++++++++++++ .../ui/pages/modplatform/flame/FlameModPage.h | 36 ++++++++++++ .../ui/pages/modplatform/flame/FlamePage.cpp | 40 +++++++++++++ .../ui/pages/modplatform/flame/FlamePage.h | 41 +++++++++---- launcher/ui/pages/modplatform/ftb/FtbPage.cpp | 46 +++++++++++---- launcher/ui/pages/modplatform/ftb/FtbPage.h | 41 +++++++++---- .../ui/pages/modplatform/legacy_ftb/Page.cpp | 40 +++++++++++++ .../ui/pages/modplatform/legacy_ftb/Page.h | 41 +++++++++---- .../modplatform/modrinth/ModrinthPage.cpp | 39 +++++++++++++ .../pages/modplatform/modrinth/ModrinthPage.h | 36 ++++++++++++ .../pages/modplatform/technic/TechnicPage.cpp | 45 ++++++++++---- .../pages/modplatform/technic/TechnicPage.h | 41 +++++++++---- launcher/ui/widgets/CustomCommands.cpp | 39 +++++++++++++ launcher/ui/widgets/CustomCommands.h | 41 +++++++++---- launcher/ui/widgets/PageContainer.cpp | 58 +++++++++++++++---- launcher/ui/widgets/PageContainer.h | 43 ++++++++++---- 64 files changed, 2098 insertions(+), 413 deletions(-) diff --git a/launcher/ui/pages/BasePage.h b/launcher/ui/pages/BasePage.h index 408965d0..ceb24040 100644 --- a/launcher/ui/pages/BasePage.h +++ b/launcher/ui/pages/BasePage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -47,6 +67,8 @@ public: { m_container = container; }; + virtual void retranslate() { } + public: int stackIndex = -1; int listIndex = -1; diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 037ec217..287eb74f 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * 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 @@ -85,3 +86,8 @@ bool APIPage::apply() applySettings(); return true; } + +void APIPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index d9a84753..20356009 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * 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 @@ -69,6 +70,7 @@ public: return "APIs"; } virtual bool apply() override; + void retranslate() override; private: void loadSettings(); diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index af1d9d60..1edba499 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * 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 @@ -104,6 +105,11 @@ AccountListPage::~AccountListPage() delete ui; } +void AccountListPage::retranslate() +{ + ui->retranslateUi(this); +} + void AccountListPage::ShowContextMenu(const QPoint& pos) { auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index c1dea8be..9395e92b 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * 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 @@ -78,6 +79,7 @@ public: { return "Getting-Started#adding-an-account"; } + void retranslate() override; public slots: void on_actionAddMojang_triggered(); diff --git a/launcher/ui/pages/global/CustomCommandsPage.cpp b/launcher/ui/pages/global/CustomCommandsPage.cpp index 8541e3c1..436d766e 100644 --- a/launcher/ui/pages/global/CustomCommandsPage.cpp +++ b/launcher/ui/pages/global/CustomCommandsPage.cpp @@ -1,3 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 @@ -49,3 +85,8 @@ void CustomCommandsPage::loadSettings() s->get("PostExitCommand").toString() ); } + +void CustomCommandsPage::retranslate() +{ + commands->retranslate(); +} diff --git a/launcher/ui/pages/global/CustomCommandsPage.h b/launcher/ui/pages/global/CustomCommandsPage.h index a1155e0e..865503ff 100644 --- a/launcher/ui/pages/global/CustomCommandsPage.h +++ b/launcher/ui/pages/global/CustomCommandsPage.h @@ -1,16 +1,36 @@ -/* Copyright 2018-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -47,6 +67,7 @@ public: return "Custom-commands"; } bool apply() override; + void retranslate() override; private: void applySettings(); diff --git a/launcher/ui/pages/global/ExternalToolsPage.cpp b/launcher/ui/pages/global/ExternalToolsPage.cpp index 41d900aa..693ca5c1 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.cpp +++ b/launcher/ui/pages/global/ExternalToolsPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "ExternalToolsPage.h" @@ -231,3 +251,8 @@ bool ExternalToolsPage::apply() applySettings(); return true; } + +void ExternalToolsPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/global/ExternalToolsPage.h b/launcher/ui/pages/global/ExternalToolsPage.h index 5ae6148d..8bd38a19 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.h +++ b/launcher/ui/pages/global/ExternalToolsPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -54,6 +74,7 @@ public: return "Tools"; } virtual bool apply() override; + void retranslate() override; private: void loadSettings(); diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index bd79f11a..3eb4bd59 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "JavaPage.h" @@ -151,3 +171,8 @@ void JavaPage::checkerFinished() { checker.reset(); } + +void JavaPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/global/JavaPage.h b/launcher/ui/pages/global/JavaPage.h index 8f9b3323..64d4098e 100644 --- a/launcher/ui/pages/global/JavaPage.h +++ b/launcher/ui/pages/global/JavaPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -54,6 +74,7 @@ public: return "Java-settings"; } bool apply() override; + void retranslate() override; private: void applySettings(); diff --git a/launcher/ui/pages/global/LanguagePage.cpp b/launcher/ui/pages/global/LanguagePage.cpp index 359fdeeb..485d7fd4 100644 --- a/launcher/ui/pages/global/LanguagePage.cpp +++ b/launcher/ui/pages/global/LanguagePage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "LanguagePage.h" #include "ui/widgets/LanguageSelectionWidget.h" @@ -40,12 +75,3 @@ void LanguagePage::retranslate() { mainWidget->retranslate(); } - -void LanguagePage::changeEvent(QEvent* event) -{ - if (event->type() == QEvent::LanguageChange) - { - retranslate(); - } - QWidget::changeEvent(event); -} diff --git a/launcher/ui/pages/global/LanguagePage.h b/launcher/ui/pages/global/LanguagePage.h index b1dd05ad..9b321170 100644 --- a/launcher/ui/pages/global/LanguagePage.h +++ b/launcher/ui/pages/global/LanguagePage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -48,12 +68,11 @@ public: } bool apply() override; - void changeEvent(QEvent * ) override; + void retranslate() override; private: void applySettings(); void loadSettings(); - void retranslate(); private: LanguageSelectionWidget *mainWidget; diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index cec496dc..64e668e9 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "LauncherPage.h" @@ -446,3 +466,8 @@ void LauncherPage::refreshFontPreview() workCursor.insertBlock(); } } + +void LauncherPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index 4d0cf3c9..63cfe9c3 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -57,6 +77,7 @@ public: return "Launcher-settings"; } bool apply() override; + void retranslate() override; private: void applySettings(); diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index 5470a586..9abae425 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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" @@ -94,3 +114,8 @@ void MinecraftPage::loadSettings() ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool()); } + +void MinecraftPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/global/MinecraftPage.h b/launcher/ui/pages/global/MinecraftPage.h index 42626d94..cf5f95eb 100644 --- a/launcher/ui/pages/global/MinecraftPage.h +++ b/launcher/ui/pages/global/MinecraftPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -54,6 +74,7 @@ public: return "Minecraft-settings"; } bool apply() override; + void retranslate() override; private: void updateCheckboxStuff(); diff --git a/launcher/ui/pages/global/ProxyPage.cpp b/launcher/ui/pages/global/ProxyPage.cpp index 5bc8199e..aefd1e74 100644 --- a/launcher/ui/pages/global/ProxyPage.cpp +++ b/launcher/ui/pages/global/ProxyPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "ProxyPage.h" @@ -104,3 +124,8 @@ void ProxyPage::loadSettings() ui->proxyUserEdit->setText(s->get("ProxyUser").toString()); ui->proxyPassEdit->setText(s->get("ProxyPass").toString()); } + +void ProxyPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/global/ProxyPage.h b/launcher/ui/pages/global/ProxyPage.h index 6698c349..e3677774 100644 --- a/launcher/ui/pages/global/ProxyPage.h +++ b/launcher/ui/pages/global/ProxyPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -51,6 +71,7 @@ public: return "Proxy-settings"; } bool apply() override; + void retranslate() override; private: void updateCheckboxStuff(); diff --git a/launcher/ui/pages/instance/GameOptionsPage.cpp b/launcher/ui/pages/instance/GameOptionsPage.cpp index 782f2ab3..63443166 100644 --- a/launcher/ui/pages/instance/GameOptionsPage.cpp +++ b/launcher/ui/pages/instance/GameOptionsPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "GameOptionsPage.h" #include "ui_GameOptionsPage.h" #include "minecraft/MinecraftInstance.h" @@ -35,3 +70,8 @@ void GameOptionsPage::closedImpl() { // m_model->unobserve(); } + +void GameOptionsPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/instance/GameOptionsPage.h b/launcher/ui/pages/instance/GameOptionsPage.h index 878903eb..de8c421e 100644 --- a/launcher/ui/pages/instance/GameOptionsPage.h +++ b/launcher/ui/pages/instance/GameOptionsPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -56,6 +76,7 @@ public: { return "Game-Options-management"; } + void retranslate() override; private: // data Ui::GameOptionsPage *ui = nullptr; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index b0e18af4..e68a7124 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -1,3 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "ui_InstanceSettingsPage.h" @@ -339,3 +375,9 @@ void InstanceSettingsPage::checkerFinished() { checker.reset(); } + +void InstanceSettingsPage::retranslate() +{ + ui->retranslateUi(this); + ui->customCommands->retranslate(); // TODO: why is this seperate from the others? +} diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 5c8c8e66..97d1296f 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -55,6 +75,7 @@ public: return "Instance-settings"; } virtual bool shouldDisplay() const override; + void retranslate() override; private slots: void on_javaDetectBtn_clicked(); diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index b66c6cc7..30a8735f 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "LogPage.h" #include "ui_LogPage.h" @@ -328,3 +363,8 @@ void LogPage::findActivated() ui->searchBar->selectAll(); } } + +void LogPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h index cab25563..f6fe87c4 100644 --- a/launcher/ui/pages/instance/LogPage.h +++ b/launcher/ui/pages/instance/LogPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -54,6 +74,7 @@ public: return "Minecraft-Logs"; } virtual bool shouldDisplay() const override; + void retranslate() override; private slots: void on_btnPaste_clicked(); diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index c7a0376d..599f0e11 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "ModFolderPage.h" @@ -252,6 +272,11 @@ bool ModFolderPage::shouldDisplay() const return true; } +void ModFolderPage::retranslate() +{ + ui->retranslateUi(this); +} + bool CoreModFolderPage::shouldDisplay() const { if (ModFolderPage::shouldDisplay()) diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index fbda3cd8..72e2d404 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -66,6 +86,7 @@ public: return m_helpName; } virtual bool shouldDisplay() const override; + void retranslate() override; virtual void openedImpl() override; virtual void closedImpl() override; diff --git a/launcher/ui/pages/instance/NotesPage.cpp b/launcher/ui/pages/instance/NotesPage.cpp index fa966c91..95a9fad2 100644 --- a/launcher/ui/pages/instance/NotesPage.cpp +++ b/launcher/ui/pages/instance/NotesPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "NotesPage.h" #include "ui_NotesPage.h" #include @@ -19,3 +54,8 @@ bool NotesPage::apply() m_inst->setNotes(ui->noteEditor->toPlainText()); return true; } + +void NotesPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/instance/NotesPage.h b/launcher/ui/pages/instance/NotesPage.h index 539401ee..80a7279b 100644 --- a/launcher/ui/pages/instance/NotesPage.h +++ b/launcher/ui/pages/instance/NotesPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -53,6 +73,7 @@ public: { return "Notes"; } + void retranslate() override; private: Ui::NotesPage *ui; diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 0131c5c1..0c1939c6 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "OtherLogsPage.h" @@ -55,6 +75,11 @@ OtherLogsPage::~OtherLogsPage() delete ui; } +void OtherLogsPage::retranslate() +{ + ui->retranslateUi(this); +} + void OtherLogsPage::openedImpl() { m_watcher->enable(); diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index b2b2a91b..95591638 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -52,6 +72,8 @@ public: { return "Minecraft-Logs"; } + void retranslate() override; + void openedImpl() override; void closedImpl() override; diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h index 1486bf52..8054926c 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.h +++ b/launcher/ui/pages/instance/ResourcePackPage.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "ModFolderPage.h" diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index 4011d88c..e694ebe3 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ScreenshotsPage.h" #include "ui_ScreenshotsPage.h" @@ -270,6 +305,11 @@ bool ScreenshotsPage::eventFilter(QObject *obj, QEvent *evt) return QWidget::eventFilter(obj, evt); } +void ScreenshotsPage::retranslate() +{ + ui->retranslateUi(this); +} + ScreenshotsPage::~ScreenshotsPage() { delete ui; diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index 2a1fdeee..50cf1a17 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -67,6 +87,7 @@ public: { return !m_uploadActive; } + void retranslate() override; protected: QMenu * createPopupMenu() override; diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 8116d2bf..2af6164c 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ServersPage.h" #include "ui_ServersPage.h" @@ -600,6 +635,11 @@ ServersPage::~ServersPage() delete ui; } +void ServersPage::retranslate() +{ + ui->retranslateUi(this); +} + void ServersPage::ShowContextMenu(const QPoint& pos) { auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h index d91da2ae..5173712c 100644 --- a/launcher/ui/pages/instance/ServersPage.h +++ b/launcher/ui/pages/instance/ServersPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -57,6 +77,7 @@ public: { return "Servers-management"; } + void retranslate() override; protected: QMenu * createPopupMenu() override; diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h index 36724992..7d4f5074 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.h +++ b/launcher/ui/pages/instance/ShaderPackPage.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "ModFolderPage.h" diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h index 3f04997d..e8cefe6e 100644 --- a/launcher/ui/pages/instance/TexturePackPage.h +++ b/launcher/ui/pages/instance/TexturePackPage.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "ModFolderPage.h" diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 43c449eb..97c6fe8f 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "Application.h" @@ -99,6 +119,11 @@ bool VersionPage::shouldDisplay() const return true; } +void VersionPage::retranslate() +{ + ui->retranslateUi(this); +} + QMenu * VersionPage::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index b5ce4064..2d37af43 100644 --- a/launcher/ui/pages/instance/VersionPage.h +++ b/launcher/ui/pages/instance/VersionPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -47,6 +67,7 @@ public: return "Instance-Version"; } virtual bool shouldDisplay() const override; + void retranslate() override; private slots: void on_actionChange_version_triggered(); diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index d2bf63bd..650583a2 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2015-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "WorldListPage.h" @@ -122,6 +142,11 @@ bool WorldListPage::shouldDisplay() const return true; } +void WorldListPage::retranslate() +{ + ui->retranslateUi(this); +} + bool WorldListPage::worldListFilter(QKeyEvent *keyEvent) { switch (keyEvent->key()) diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h index e07d5794..17e36a08 100644 --- a/launcher/ui/pages/instance/WorldListPage.h +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -1,16 +1,36 @@ -/* Copyright 2015-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -57,6 +77,7 @@ public: return "Worlds"; } virtual bool shouldDisplay() const override; + void retranslate() override; virtual void openedImpl() override; virtual void closedImpl() override; diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index c9e24ead..487bf77b 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ImportPage.h" #include "ui_ImportPage.h" @@ -50,6 +85,11 @@ bool ImportPage::shouldDisplay() const return true; } +void ImportPage::retranslate() +{ + ui->retranslateUi(this); +} + void ImportPage::openedImpl() { updateState(); diff --git a/launcher/ui/pages/modplatform/ImportPage.h b/launcher/ui/pages/modplatform/ImportPage.h index aba4def0..8d13ac10 100644 --- a/launcher/ui/pages/modplatform/ImportPage.h +++ b/launcher/ui/pages/modplatform/ImportPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -52,6 +72,7 @@ public: return "Zip-import"; } virtual bool shouldDisplay() const override; + void retranslate() override; void setUrl(const QString & url); void openedImpl() override; diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/VanillaPage.cpp index 5c58c1f1..c691128f 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.cpp +++ b/launcher/ui/pages/modplatform/VanillaPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "VanillaPage.h" #include "ui_VanillaPage.h" @@ -74,6 +109,11 @@ bool VanillaPage::shouldDisplay() const return true; } +void VanillaPage::retranslate() +{ + ui->retranslateUi(this); +} + BaseVersionPtr VanillaPage::selectedVersion() const { return m_selectedVersion; diff --git a/launcher/ui/pages/modplatform/VanillaPage.h b/launcher/ui/pages/modplatform/VanillaPage.h index fd4c2daa..4e7479df 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.h +++ b/launcher/ui/pages/modplatform/VanillaPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -52,6 +72,8 @@ public: return "Vanilla-platform"; } virtual bool shouldDisplay() const override; + void retranslate() override; + void openedImpl() override; BaseVersionPtr selectedVersion() const; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index af0cc8d6..df9b9207 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield - * Copyright 2021 Philip T + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Philip T + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "AtlPage.h" @@ -65,6 +84,11 @@ bool AtlPage::shouldDisplay() const return true; } +void AtlPage::retranslate() +{ + ui->retranslateUi(this); +} + void AtlPage::openedImpl() { if(!initialized) diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index 5b3f2228..d5f622aa 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -1,17 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * + * 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 @@ -57,6 +76,7 @@ public: return "ATL-platform"; } virtual bool shouldDisplay() const override; + void retranslate() override; void openedImpl() override; diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 114ac907..a4dc1088 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "FlameModPage.h" #include "ui_FlameModPage.h" @@ -60,6 +95,11 @@ bool FlameModPage::eventFilter(QObject *watched, QEvent *event) { bool FlameModPage::shouldDisplay() const { return true; } +void FlameModPage::retranslate() +{ + ui->retranslateUi(this); +} + void FlameModPage::openedImpl() { updateSelectionButton(); triggerSearch(); diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index b5b19a4f..e0ceb69c 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 @@ -42,6 +77,7 @@ public: return "Flame-platform"; } virtual bool shouldDisplay() const override; + void retranslate() override; void openedImpl() override; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 7e6ac2fd..cbe709c2 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "FlamePage.h" #include "ui_FlamePage.h" @@ -57,6 +92,11 @@ bool FlamePage::shouldDisplay() const return true; } +void FlamePage::retranslate() +{ + ui->retranslateUi(this); +} + void FlamePage::openedImpl() { suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 5cfe21dc..1695777a 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -57,6 +77,7 @@ public: return "Flame-platform"; } virtual bool shouldDisplay() const override; + void retranslate() override; void openedImpl() override; diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp index b6b5dcd4..8a93bc2e 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield - * Copyright 2021 Philip T + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Philip T + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "FtbPage.h" @@ -78,6 +97,11 @@ bool FtbPage::shouldDisplay() const return true; } +void FtbPage::retranslate() +{ + ui->retranslateUi(this); +} + void FtbPage::openedImpl() { if(!initialised) diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.h b/launcher/ui/pages/modplatform/ftb/FtbPage.h index 28a189f0..e2dbca02 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.h +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -55,6 +75,7 @@ public: return "FTB-platform"; } virtual bool shouldDisplay() const override; + void retranslate() override; void openedImpl() override; diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 891704de..27a12cda 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "Page.h" #include "ui_Page.h" @@ -122,6 +157,11 @@ void Page::openedImpl() suggestCurrent(); } +void Page::retranslate() +{ + ui->retranslateUi(this); +} + void Page::suggestCurrent() { if(!isOpened) diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index d8225e11..6f36e89b 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -66,6 +86,7 @@ public: } bool shouldDisplay() const override; void openedImpl() override; + void retranslate() override; private: void suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 35cd743a..2643c313 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "ModrinthPage.h" #include "ui_ModrinthPage.h" @@ -59,6 +94,10 @@ bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) { bool ModrinthPage::shouldDisplay() const { return true; } +void ModrinthPage::retranslate() { + ui->retranslateUi(this); +} + void ModrinthPage::openedImpl() { updateSelectionButton(); triggerSearch(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 52b538e3..5002ae42 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 @@ -42,6 +77,7 @@ public: return "Modrinth-platform"; } virtual bool shouldDisplay() const override; + void retranslate() override; void openedImpl() override; diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 67f6e52c..c3807269 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "TechnicPage.h" @@ -61,6 +81,11 @@ bool TechnicPage::shouldDisplay() const return true; } +void TechnicPage::retranslate() +{ + ui->retranslateUi(this); +} + void TechnicPage::openedImpl() { suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index 21695dd0..24dd2935 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -57,6 +77,7 @@ public: return "Technic-platform"; } virtual bool shouldDisplay() const override; + void retranslate() override; void openedImpl() override; diff --git a/launcher/ui/widgets/CustomCommands.cpp b/launcher/ui/widgets/CustomCommands.cpp index 24bdc07d..5a718b54 100644 --- a/launcher/ui/widgets/CustomCommands.cpp +++ b/launcher/ui/widgets/CustomCommands.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "CustomCommands.h" #include "ui_CustomCommands.h" @@ -26,6 +61,10 @@ void CustomCommands::initialize(bool checkable, bool checked, const QString& pre } +void CustomCommands::retranslate() { + ui->retranslateUi(this); +} + bool CustomCommands::checked() const { if(!ui->customCommandsGroupBox->isCheckable()) diff --git a/launcher/ui/widgets/CustomCommands.h b/launcher/ui/widgets/CustomCommands.h index 8db991fa..4a7a17ef 100644 --- a/launcher/ui/widgets/CustomCommands.h +++ b/launcher/ui/widgets/CustomCommands.h @@ -1,16 +1,36 @@ -/* Copyright 2018-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -31,6 +51,7 @@ public: virtual ~CustomCommands(); void initialize(bool checkable, bool checked, const QString & prelaunch, const QString & wrapper, const QString & postexit); + void retranslate(); bool checked() const; QString prelaunchCommand() const; QString wrapperCommand() const; diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 558a98fb..2af7d731 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -1,16 +1,37 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "PageContainer.h" @@ -162,6 +183,15 @@ void PageContainer::createUI() setLayout(m_layout); } +void PageContainer::retranslate() +{ + if (m_currentPage) + m_header->setText(m_currentPage->displayName()); + + for (auto page : m_model->pages()) + page->retranslate(); +} + void PageContainer::addButtons(QWidget *buttons) { m_layout->addWidget(buttons, 2, 0, 1, 2); @@ -239,3 +269,11 @@ bool PageContainer::saveAll() } return true; } + +void PageContainer::changeEvent(QEvent* event) +{ + if (event->type() == QEvent::LanguageChange) { + retranslate(); + } + QWidget::changeEvent(event); +} diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h index 8d2172db..86f549eb 100644 --- a/launcher/ui/widgets/PageContainer.h +++ b/launcher/ui/widgets/PageContainer.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -66,8 +86,11 @@ public: m_container = container; }; + void changeEvent(QEvent*) override; + private: void createUI(); + void retranslate(); public slots: void help(); From a5fc640f2cb0ca772efdf93f400084d33f6383d4 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 21 Mar 2022 14:21:06 +0100 Subject: [PATCH 140/605] Merge pull request #298 from Scrumplex/fix-i18n Fix translatable strings --- launcher/InstanceImportTask.cpp | 2 +- launcher/JavaCommon.cpp | 8 ++++---- launcher/minecraft/launch/DirectJavaLaunch.cpp | 2 +- launcher/minecraft/launch/LauncherPartLaunch.cpp | 2 +- launcher/minecraft/update/FoldersTask.cpp | 2 +- launcher/modplatform/legacy_ftb/PackInstallTask.cpp | 2 +- .../modplatform/technic/TechnicPackProcessor.cpp | 2 +- launcher/ui/MainWindow.cpp | 12 ++++++------ launcher/ui/pages/global/APIPage.ui | 2 +- launcher/ui/pages/modplatform/atlauncher/AtlPage.h | 2 +- launcher/ui/pages/modplatform/atlauncher/AtlPage.ui | 2 +- launcher/ui/pages/modplatform/flame/FlameModPage.cpp | 4 ++-- launcher/ui/pages/modplatform/flame/FlameModPage.h | 2 +- launcher/ui/pages/modplatform/flame/FlameModPage.ui | 2 +- launcher/ui/pages/modplatform/flame/FlamePage.h | 2 +- launcher/ui/pages/modplatform/flame/FlamePage.ui | 2 +- launcher/ui/pages/modplatform/ftb/FtbPage.h | 2 +- launcher/ui/pages/modplatform/ftb/FtbPage.ui | 2 +- launcher/ui/pages/modplatform/legacy_ftb/Page.h | 2 +- .../ui/pages/modplatform/modrinth/ModrinthPage.cpp | 6 +++--- .../ui/pages/modplatform/modrinth/ModrinthPage.h | 2 +- .../ui/pages/modplatform/modrinth/ModrinthPage.ui | 2 +- launcher/ui/pages/modplatform/technic/TechnicPage.h | 2 +- launcher/ui/pages/modplatform/technic/TechnicPage.ui | 2 +- launcher/ui/setupwizard/JavaWizardPage.cpp | 2 +- launcher/ui/widgets/CustomCommands.ui | 2 +- launcher/ui/widgets/JavaSettingsWidget.cpp | 4 ++-- 27 files changed, 39 insertions(+), 39 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 6dd615c7..a825e8d4 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -283,7 +283,7 @@ void InstanceImportTask::processFlame() } else { - logWarning(tr("Could not map recommended forge version for Minecraft %1").arg(mcVersion)); + logWarning(tr("Could not map recommended Forge version for Minecraft %1").arg(mcVersion)); } } components->setComponentVersion("net.minecraftforge", forgeVersion); diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index 6fa5851b..a6542fa7 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -8,7 +8,7 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent) || 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\" or \"-Xms\").\n" + "You tried to manually set a JVM memory option (using \"-XX:PermSize\", \"-XX-MaxHeapSize\", \"-XX:InitialHeapSize\", \"-Xmx\" or \"-Xms\").\n" "There are dedicated boxes for these in the settings (Java tab, in the Memory group at the top).\n" "This message will be displayed until you remove them from the JVM arguments."); CustomMessageBox::selectable( @@ -40,7 +40,7 @@ void JavaCommon::javaArgsWereBad(QWidget *parent, JavaCheckResult result) auto htmlError = result.errorLog; QString text; htmlError.replace('\n', "
"); - text += QObject::tr("The specified java binary didn't work with the arguments you provided:
"); + text += QObject::tr("The specified Java binary didn't work with the arguments you provided:
"); text += QString("%1").arg(htmlError); CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } @@ -49,8 +49,8 @@ void JavaCommon::javaBinaryWasBad(QWidget *parent, JavaCheckResult result) { QString text; text += QObject::tr( - "The specified java binary didn't work.
You should use the auto-detect feature, " - "or set the path to the java executable.
"); + "The specified Java binary didn't work.
You should use the auto-detect feature, " + "or set the path to the Java executable.
"); CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } diff --git a/launcher/minecraft/launch/DirectJavaLaunch.cpp b/launcher/minecraft/launch/DirectJavaLaunch.cpp index 2bcff664..742170fa 100644 --- a/launcher/minecraft/launch/DirectJavaLaunch.cpp +++ b/launcher/minecraft/launch/DirectJavaLaunch.cpp @@ -88,7 +88,7 @@ void DirectJavaLaunch::on_state(LoggedProcess::State state) case LoggedProcess::FailedToStart: { //: Error message displayed if instance can't start - const char *reason = QT_TR_NOOP("Could not launch minecraft!"); + const char *reason = QT_TR_NOOP("Could not launch Minecraft!"); emit logLine(reason, MessageLevel::Fatal); emitFailed(tr(reason)); return; diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index f461b847..d15d7e9d 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -154,7 +154,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) case LoggedProcess::FailedToStart: { //: Error message displayed if instace can't start - const char *reason = QT_TR_NOOP("Could not launch minecraft!"); + const char *reason = QT_TR_NOOP("Could not launch Minecraft!"); emit logLine(reason, MessageLevel::Fatal); emitFailed(tr(reason)); return; diff --git a/launcher/minecraft/update/FoldersTask.cpp b/launcher/minecraft/update/FoldersTask.cpp index e2b1bb48..22768bd9 100644 --- a/launcher/minecraft/update/FoldersTask.cpp +++ b/launcher/minecraft/update/FoldersTask.cpp @@ -14,7 +14,7 @@ void FoldersTask::executeTask() QDir mcDir(m_inst->gameRoot()); if (!mcDir.exists() && !mcDir.mkpath(".")) { - emitFailed(tr("Failed to create folder for minecraft binaries.")); + emitFailed(tr("Failed to create folder for Minecraft binaries.")); return; } emitSucceeded(); diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index f655a066..c63a9f1e 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -114,7 +114,7 @@ void PackInstallTask::install() //ok, found minecraft dir, move contents to instance dir if(!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft")) { - emitFailed(tr("Failed to move unzipped minecraft!")); + emitFailed(tr("Failed to move unzipped Minecraft!")); return; } } diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index 156a295a..782fb9b2 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -88,7 +88,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const else { if (minecraftVersion.isEmpty()) - emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but minecraft version is unknown")); + emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown")); components->setComponentVersion("net.minecraft", minecraftVersion, true); components->installJarMods({modpackJar}); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 2cb3c7fd..4be509b4 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -349,7 +349,7 @@ public: actionMATRIX = TranslatedAction(MainWindow); actionMATRIX->setObjectName(QStringLiteral("actionMATRIX")); actionMATRIX->setIcon(APPLICATION->getThemedIcon("matrix")); - actionMATRIX.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Matrix")); + actionMATRIX.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Matrix space")); actionMATRIX.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Matrix space")); all_actions.append(&actionMATRIX); helpMenu->addAction(actionMATRIX); @@ -359,7 +359,7 @@ public: actionDISCORD = TranslatedAction(MainWindow); actionDISCORD->setObjectName(QStringLiteral("actionDISCORD")); actionDISCORD->setIcon(APPLICATION->getThemedIcon("discord")); - actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Discord")); + actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Discord guild")); actionDISCORD.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Discord guild.")); all_actions.append(&actionDISCORD); helpMenu->addAction(actionDISCORD); @@ -369,7 +369,7 @@ public: actionREDDIT = TranslatedAction(MainWindow); actionREDDIT->setObjectName(QStringLiteral("actionREDDIT")); actionREDDIT->setIcon(APPLICATION->getThemedIcon("reddit-alien")); - actionREDDIT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Reddit")); + actionREDDIT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Subreddit")); actionREDDIT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 subreddit.")); all_actions.append(&actionREDDIT); helpMenu->addAction(actionREDDIT); @@ -567,7 +567,7 @@ public: actionViewSelectedMCFolder = TranslatedAction(MainWindow); actionViewSelectedMCFolder->setObjectName(QStringLiteral("actionViewSelectedMCFolder")); actionViewSelectedMCFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Minecraft Folder")); - actionViewSelectedMCFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's minecraft folder in a file browser.")); + actionViewSelectedMCFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's Minecraft folder in a file browser.")); all_actions.append(&actionViewSelectedMCFolder); instanceToolBar->addAction(actionViewSelectedMCFolder); @@ -605,7 +605,7 @@ public: actionDeleteInstance = TranslatedAction(MainWindow); actionDeleteInstance->setObjectName(QStringLiteral("actionDeleteInstance")); - actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Delete")); + actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Delete Instance")); actionDeleteInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Delete the selected instance.")); all_actions.append(&actionDeleteInstance); instanceToolBar->addAction(actionDeleteInstance); @@ -1598,7 +1598,7 @@ void MainWindow::deleteGroup() QString groupName = map["group"].toString(); if(!groupName.isEmpty()) { - auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1") + auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1?") .arg(groupName), QMessageBox::Yes | QMessageBox::No); if(reply == QMessageBox::Yes) { diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 1bc41e5a..7a9088d1 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -71,7 +71,7 @@ - https://0x0.st + https://0x0.st
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index d5f622aa..c95b0127 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -61,7 +61,7 @@ public: virtual ~AtlPage(); virtual QString displayName() const override { - return tr("ATLauncher"); + return "ATLauncher"; } virtual QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui index 9085766a..746aa6d1 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui @@ -71,7 +71,7 @@ - Search and filter ... + Search and filter... true diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index a4dc1088..d1641729 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -193,7 +193,7 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) { ui->versionSelectionBox->addItem(version.version, QVariant(i)); } if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No Valid Version found!"), + ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); } @@ -211,7 +211,7 @@ void FlameModPage::onSelectionChanged(QModelIndex first, QModelIndex second) { QVariant(i)); } if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No Valid Version found!"), + ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index e0ceb69c..2a6ade85 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -62,7 +62,7 @@ public: virtual ~FlameModPage(); virtual QString displayName() const override { - return tr("CurseForge"); + return "CurseForge"; } virtual QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.ui b/launcher/ui/pages/modplatform/flame/FlameModPage.ui index 36df7e8a..25cb2571 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.ui +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.ui @@ -41,7 +41,7 @@ - Search and filter ... + Search and filter... diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 1695777a..baac57c9 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -62,7 +62,7 @@ public: virtual ~FlamePage(); virtual QString displayName() const override { - return tr("CurseForge"); + return "CurseForge"; } virtual QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index 9723815a..6d8d8e10 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -71,7 +71,7 @@ - Search and filter ... + Search and filter... diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.h b/launcher/ui/pages/modplatform/ftb/FtbPage.h index e2dbca02..90c8e7fd 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.h +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.h @@ -60,7 +60,7 @@ public: virtual ~FtbPage(); virtual QString displayName() const override { - return tr("FTB"); + return "FTB"; } virtual QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.ui b/launcher/ui/pages/modplatform/ftb/FtbPage.ui index e9c783e3..850bf091 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.ui +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.ui @@ -34,7 +34,7 @@ - Search and filter ... + Search and filter... true diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index 6f36e89b..52db7d91 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -70,7 +70,7 @@ public: virtual ~Page(); QString displayName() const override { - return tr("FTB Legacy"); + return "FTB Legacy"; } QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 2643c313..82340448 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -62,7 +62,7 @@ ModrinthPage::ModrinthPage(ModDownloadDialog *dialog, BaseInstance *instance) ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); // index is used to set the sorting with the modrinth api - ui->sortByBox->addItem(tr("Sort by Relevence")); + ui->sortByBox->addItem(tr("Sort by Relevance")); ui->sortByBox->addItem(tr("Sort by Downloads")); ui->sortByBox->addItem(tr("Sort by Follows")); ui->sortByBox->addItem(tr("Sort by last updated")); @@ -178,7 +178,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { ui->versionSelectionBox->addItem(version.version, QVariant(i)); } if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No Valid Version found !"), + ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); } @@ -198,7 +198,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { QVariant(i)); } if (ui->versionSelectionBox->count() == 0) { - ui->versionSelectionBox->addItem(tr("No Valid Version found !"), + ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 5002ae42..92955d62 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -62,7 +62,7 @@ public: virtual ~ModrinthPage(); virtual QString displayName() const override { - return tr("Modrinth"); + return "Modrinth"; } virtual QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index d0a8b8f7..6c709825 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -51,7 +51,7 @@ - Search and filter ... + Search and filter... diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index 24dd2935..bf4baa58 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -62,7 +62,7 @@ public: virtual ~TechnicPage(); virtual QString displayName() const override { - return tr("Technic"); + return "Technic"; } virtual QIcon icon() const override { diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index dde685d9..62ab6154 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -29,7 +29,7 @@ - Search and filter ... + Search and filter... diff --git a/launcher/ui/setupwizard/JavaWizardPage.cpp b/launcher/ui/setupwizard/JavaWizardPage.cpp index 63b3d480..14683778 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.cpp +++ b/launcher/ui/setupwizard/JavaWizardPage.cpp @@ -93,6 +93,6 @@ void JavaWizardPage::retranslate() { setTitle(tr("Java")); setSubTitle(tr("You do not have a working Java set up yet or it went missing.\n" - "Please select one of the following or browse for a java executable.")); + "Please select one of the following or browse for a Java executable.")); m_java_widget->retranslate(); } diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index 21964ad2..dbd54431 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -74,7 +74,7 @@ - <html><head/><body><p>Pre-launch command runs before the instance launches and post-exit command runs after it exits.</p><p>Both will be run in the launcher's working folder with extra environment variables:</p><ul><li>$INST_NAME - Name of the instance</li><li>$INST_ID - ID of the instance (its folder name)</li><li>$INST_DIR - absolute path of the instance</li><li>$INST_MC_DIR - absolute path of minecraft</li><li>$INST_JAVA - java binary used for launch</li><li>$INST_JAVA_ARGS - command-line parameters used for launch</li></ul><p>Wrapper command allows launching using an extra wrapper program (like 'optirun' on Linux)</p></body></html> + <html><head/><body><p>Pre-launch command runs before the instance launches and post-exit command runs after it exits.</p><p>Both will be run in the launcher's working folder with extra environment variables:</p><ul><li>$INST_NAME - Name of the instance</li><li>$INST_ID - ID of the instance (its folder name)</li><li>$INST_DIR - absolute path of the instance</li><li>$INST_MC_DIR - absolute path of Minecraft</li><li>$INST_JAVA - Java binary used for launch</li><li>$INST_JAVA_ARGS - command-line parameters used for launch</li></ul><p>Wrapper command allows launching using an extra wrapper program (like 'optirun' on Linux)</p></body></html> Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index ed07e082..340518b1 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -287,7 +287,7 @@ void JavaSettingsWidget::on_javaStatusBtn_clicked() break; case JavaStatus::DoesNotStart: { - text += QObject::tr("The specified java binary didn't start properly.
"); + text += QObject::tr("The specified Java binary didn't start properly.
"); auto htmlError = m_result.errorLog; if(!htmlError.isEmpty()) { @@ -299,7 +299,7 @@ void JavaSettingsWidget::on_javaStatusBtn_clicked() } case JavaStatus::ReturnedInvalidData: { - text += QObject::tr("The specified java binary returned unexpected results:
"); + text += QObject::tr("The specified Java binary returned unexpected results:
"); auto htmlOut = m_result.outLog; if(!htmlOut.isEmpty()) { From ce05ce92bb7f772963526c60e47382579728344b Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 21 Mar 2022 16:58:37 +0100 Subject: [PATCH 141/605] Merge pull request #322 from oynqr/build/static-rainbow Build with static rainbow --- launcher/CMakeLists.txt | 2 +- libraries/rainbow/CMakeLists.txt | 14 +++--------- libraries/rainbow/include/rainbow.h | 20 ++++++++--------- libraries/rainbow/include/rainbow_config.h | 26 ---------------------- 4 files changed, 13 insertions(+), 49 deletions(-) delete mode 100644 libraries/rainbow/include/rainbow_config.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 94dffb2f..6457ae74 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -952,8 +952,8 @@ target_link_libraries(Launcher_logic Launcher_iconfix QuaZip::QuaZip hoedown - PolyMC_rainbow LocalPeer + Launcher_rainbow ) target_link_libraries(Launcher_logic) diff --git a/libraries/rainbow/CMakeLists.txt b/libraries/rainbow/CMakeLists.txt index a07135c3..e57dbbc2 100644 --- a/libraries/rainbow/CMakeLists.txt +++ b/libraries/rainbow/CMakeLists.txt @@ -8,15 +8,7 @@ set(RAINBOW_SOURCES src/rainbow.cpp ) -add_definitions(-DRAINBOW_LIBRARY) -add_library(PolyMC_rainbow SHARED ${RAINBOW_SOURCES}) -target_include_directories(PolyMC_rainbow PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") +add_library(Launcher_rainbow STATIC ${RAINBOW_SOURCES}) +target_include_directories(Launcher_rainbow PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(PolyMC_rainbow Qt5::Core Qt5::Gui) - -# Install it -install( - TARGETS PolyMC_rainbow - RUNTIME DESTINATION ${LIBRARY_DEST_DIR} - LIBRARY DESTINATION ${LIBRARY_DEST_DIR} -) +target_link_libraries(Launcher_rainbow Qt5::Core Qt5::Gui) diff --git a/libraries/rainbow/include/rainbow.h b/libraries/rainbow/include/rainbow.h index 67c46300..57be87f1 100644 --- a/libraries/rainbow/include/rainbow.h +++ b/libraries/rainbow/include/rainbow.h @@ -23,8 +23,6 @@ #pragma once -#include "rainbow_config.h" - #include class QColor; @@ -43,13 +41,13 @@ namespace Rainbow * * @see http://en.wikipedia.org/wiki/Luma_(video) */ -RAINBOW_EXPORT qreal luma(const QColor &); +qreal luma(const QColor &); /** * Calculate hue, chroma and luma of a color in one call. * @since 5.0 */ -RAINBOW_EXPORT void getHcy(const QColor &, qreal *hue, qreal *chroma, qreal *luma, +void getHcy(const QColor &, qreal *hue, qreal *chroma, qreal *luma, qreal *alpha = 0); /** @@ -64,7 +62,7 @@ RAINBOW_EXPORT void getHcy(const QColor &, qreal *hue, qreal *chroma, qreal *lum * * @see Rainbow::luma */ -RAINBOW_EXPORT qreal contrastRatio(const QColor &, const QColor &); +qreal contrastRatio(const QColor &, const QColor &); /** * Adjust the luma of a color by changing its distance from white. @@ -81,7 +79,7 @@ RAINBOW_EXPORT qreal contrastRatio(const QColor &, const QColor &); * component of the color; 1.0 means no change, 0.0 maximizes chroma * @see Rainbow::shade */ -RAINBOW_EXPORT QColor +QColor lighten(const QColor &, qreal amount = 0.5, qreal chromaInverseGain = 1.0); /** @@ -99,7 +97,7 @@ lighten(const QColor &, qreal amount = 0.5, qreal chromaInverseGain = 1.0); * component of the color; 1.0 means no change, 0.0 minimizes chroma * @see Rainbow::shade */ -RAINBOW_EXPORT QColor darken(const QColor &, qreal amount = 0.5, qreal chromaGain = 1.0); +QColor darken(const QColor &, qreal amount = 0.5, qreal chromaGain = 1.0); /** * Adjust the luma and chroma components of a color. The amount is added @@ -113,7 +111,7 @@ RAINBOW_EXPORT QColor darken(const QColor &, qreal amount = 0.5, qreal chromaGai * 1.0 maximizes chroma * @see Rainbow::luma */ -RAINBOW_EXPORT QColor shade(const QColor &, qreal lumaAmount, qreal chromaAmount = 0.0); +QColor shade(const QColor &, qreal lumaAmount, qreal chromaAmount = 0.0); /** * Create a new color by tinting one color with another. This function is @@ -127,7 +125,7 @@ RAINBOW_EXPORT QColor shade(const QColor &, qreal lumaAmount, qreal chromaAmount * @param amount how strongly to tint the base; 0.0 gives @p base, * 1.0 gives @p color */ -RAINBOW_EXPORT QColor tint(const QColor &base, const QColor &color, qreal amount = 0.3); +QColor tint(const QColor &base, const QColor &color, qreal amount = 0.3); /** * Blend two colors into a new color by linear combination. @@ -140,7 +138,7 @@ RAINBOW_EXPORT QColor tint(const QColor &base, const QColor &color, qreal amount * @p bias >= 1 gives @p c2. @p bias == 0.5 gives a 50% blend of @p c1 * and @p c2. */ -RAINBOW_EXPORT QColor mix(const QColor &c1, const QColor &c2, qreal bias = 0.5); +QColor mix(const QColor &c1, const QColor &c2, qreal bias = 0.5); /** * Blend two colors into a new color by painting the second color over the @@ -154,7 +152,7 @@ RAINBOW_EXPORT QColor mix(const QColor &c1, const QColor &c2, qreal bias = 0.5); * @param paint the color to be overlayed onto the base color. * @param comp the CompositionMode used to do the blending. */ -RAINBOW_EXPORT QColor +QColor overlayColors(const QColor &base, const QColor &paint, QPainter::CompositionMode comp = QPainter::CompositionMode_SourceOver); } diff --git a/libraries/rainbow/include/rainbow_config.h b/libraries/rainbow/include/rainbow_config.h deleted file mode 100644 index 52cc7388..00000000 --- a/libraries/rainbow/include/rainbow_config.h +++ /dev/null @@ -1,26 +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 - -#ifdef RAINBOW_STATIC - #define RAINBOW_EXPORT -#else - #ifdef RAINBOW_LIBRARY - #define RAINBOW_EXPORT Q_DECL_EXPORT - #else - #define RAINBOW_EXPORT Q_DECL_IMPORT - #endif -#endif \ No newline at end of file From 6025cd0ca5d55f58bf1079bfe87c77d7afdc34e0 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 22 Mar 2022 16:42:40 +0100 Subject: [PATCH 142/605] Merge pull request #330 from DioEgizio/notportable bring back not portable windows builds --- .github/workflows/build.yml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac181079..4aa316f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,10 +27,22 @@ jobs: - os: windows-2022 name: "Windows-i686" msystem: mingw32 + portable: false - os: windows-2022 name: "Windows-x86_64" msystem: mingw64 + portable: false + + - os: windows-2022 + name: "Windows-i686-portable" + msystem: mingw32 + portable: true + + - os: windows-2022 + name: "Windows-x86_64-portable" + msystem: mingw64 + portable: true - os: macos-11 qt_version: 5.12.12 @@ -122,11 +134,18 @@ jobs: run: | cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja - - name: Configure CMake on Windows - if: runner.os == 'Windows' + - name: Configure CMake on Windows + if: runner.os == 'Windows' && matrix.portable != true shell: msys2 {0} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_PORTABLE=OFF -G Ninja + + + - name: Configure CMake on Windows portable + if: runner.os == 'Windows' && matrix.portable == true + shell: msys2 {0} + run: | + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja - name: Configure CMake on Linux if: runner.os == 'Linux' From c7c83a35fa74b419145f2e5013873bf98bf46df5 Mon Sep 17 00:00:00 2001 From: Philipp David Date: Sat, 19 Mar 2022 18:08:28 +0100 Subject: [PATCH 143/605] Enable LTO/IPO on release builds --- .gitmodules | 4 ++-- CMakeLists.txt | 14 +++++++++++++- libraries/LocalPeer/CMakeLists.txt | 2 +- libraries/iconfix/CMakeLists.txt | 2 +- libraries/javacheck/CMakeLists.txt | 2 +- libraries/katabasis/CMakeLists.txt | 2 +- libraries/launcher/CMakeLists.txt | 2 +- libraries/libnbtplusplus | 2 +- libraries/optional-bare/CMakeLists.txt | 2 +- libraries/rainbow/CMakeLists.txt | 2 +- libraries/xz-embedded/CMakeLists.txt | 2 +- 11 files changed, 24 insertions(+), 12 deletions(-) diff --git a/.gitmodules b/.gitmodules index 10575207..08b94c96 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "depends/libnbtplusplus"] path = libraries/libnbtplusplus - url = https://github.com/MultiMC/libnbtplusplus.git - pushurl = git@github.com:MultiMC/libnbtplusplus.git + url = https://github.com/PolyMC/libnbtplusplus.git + pushurl = git@github.com:PolyMC/libnbtplusplus.git [submodule "libraries/quazip"] path = libraries/quazip diff --git a/CMakeLists.txt b/CMakeLists.txt index 7537703c..889d6fc4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.9.4) if(WIN32) # In Qt 5.1+ we have our own main() function, don't autolink to qtmain on Windows @@ -43,6 +43,18 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror=return-type") # Fix build with Qt 5.13 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") +include(CheckIPOSupported) +check_ipo_supported(RESULT ipo_supported OUTPUT ipo_error) + +if(ipo_supported AND (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")) + message(STATUS "IPO / LTO enabled") + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) +elseif(ipo_supported) + message(STATUS "Not enabling IPO / LTO on debug builds") +else() + message(STATUS "IPO / LTO not supported: <${ipo_error}>") +endif() + ##################################### Set Application options ##################################### ######## Set URLs ######## diff --git a/libraries/LocalPeer/CMakeLists.txt b/libraries/LocalPeer/CMakeLists.txt index 1e7557ec..0b434803 100644 --- a/libraries/LocalPeer/CMakeLists.txt +++ b/libraries/LocalPeer/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.9.4) project(LocalPeer) find_package(Qt5 COMPONENTS Core Network REQUIRED) diff --git a/libraries/iconfix/CMakeLists.txt b/libraries/iconfix/CMakeLists.txt index 049879c4..08441203 100644 --- a/libraries/iconfix/CMakeLists.txt +++ b/libraries/iconfix/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.9.4) project(iconfix) find_package(Qt5Core REQUIRED QUIET) diff --git a/libraries/javacheck/CMakeLists.txt b/libraries/javacheck/CMakeLists.txt index f599bf15..735de443 100644 --- a/libraries/javacheck/CMakeLists.txt +++ b/libraries/javacheck/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.9.4) project(launcher Java) find_package(Java 1.7 REQUIRED COMPONENTS Development) diff --git a/libraries/katabasis/CMakeLists.txt b/libraries/katabasis/CMakeLists.txt index d579dc29..77db286a 100644 --- a/libraries/katabasis/CMakeLists.txt +++ b/libraries/katabasis/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.6) +cmake_minimum_required(VERSION 3.9.4) string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD) if(IS_IN_SOURCE_BUILD) diff --git a/libraries/launcher/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index 54913fd4..0eccae8b 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.9.4) project(launcher Java) find_package(Java 1.7 REQUIRED COMPONENTS Development) diff --git a/libraries/libnbtplusplus b/libraries/libnbtplusplus index dc72a20b..b156bcaa 160000 --- a/libraries/libnbtplusplus +++ b/libraries/libnbtplusplus @@ -1 +1 @@ -Subproject commit dc72a20b7efd304d12af2025223fad07b4b78464 +Subproject commit b156bcaa4acf0a5b392bbed60bd274c39e2398d4 diff --git a/libraries/optional-bare/CMakeLists.txt b/libraries/optional-bare/CMakeLists.txt index b8b498c5..952df6e2 100644 --- a/libraries/optional-bare/CMakeLists.txt +++ b/libraries/optional-bare/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.9.4) project(optional-bare) add_library(optional-bare INTERFACE) diff --git a/libraries/rainbow/CMakeLists.txt b/libraries/rainbow/CMakeLists.txt index e57dbbc2..94cc1b49 100644 --- a/libraries/rainbow/CMakeLists.txt +++ b/libraries/rainbow/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.9.4) project(rainbow) find_package(Qt5Core REQUIRED QUIET) diff --git a/libraries/xz-embedded/CMakeLists.txt b/libraries/xz-embedded/CMakeLists.txt index 86ac60c8..4ce46102 100644 --- a/libraries/xz-embedded/CMakeLists.txt +++ b/libraries/xz-embedded/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.9.4) project(xz-embedded LANGUAGES C) option(XZ_BUILD_BCJ "Build xz-embedded with BCJ support (native binary optimization)" OFF) From f3a244e90a528196ce8c0d2c13f747d9fb4dbcf9 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 22 Mar 2022 19:37:10 -0300 Subject: [PATCH 144/605] fix: fix skipping one on file counting in mod version parse --- launcher/modplatform/modrinth/ModrinthPackIndex.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 9581ca04..992d6657 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -55,7 +55,8 @@ void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray // Find correct file (needed in cases where one version may have multiple files) // Will default to the last one if there's no primary (though I think Modrinth requires that // at least one file is primary, idk) - while (i < files.count()){ + // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed + while (i < files.count() - 1){ auto parent = files[i].toObject(); auto fileName = Json::requireString(parent, "filename"); @@ -77,6 +78,7 @@ void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray i++; } + auto parent = files[i].toObject(); if(parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); From dfa5f614aaf8c8e1843c5224e20d5349efb04fa0 Mon Sep 17 00:00:00 2001 From: Philipp David Date: Wed, 23 Mar 2022 10:05:31 +0100 Subject: [PATCH 145/605] Put LTO behind an optional flag --- CMakeLists.txt | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 889d6fc4..9ff47353 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,16 +43,20 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror=return-type") # Fix build with Qt 5.13 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") -include(CheckIPOSupported) -check_ipo_supported(RESULT ipo_supported OUTPUT ipo_error) +option(ENABLE_LTO "Enable Link Time Optimization" off) -if(ipo_supported AND (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")) - message(STATUS "IPO / LTO enabled") - set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) -elseif(ipo_supported) - message(STATUS "Not enabling IPO / LTO on debug builds") -else() - message(STATUS "IPO / LTO not supported: <${ipo_error}>") +if(ENABLE_LTO) + include(CheckIPOSupported) + check_ipo_supported(RESULT ipo_supported OUTPUT ipo_error) + + if(ipo_supported AND (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")) + message(STATUS "IPO / LTO enabled") + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) + elseif(ipo_supported) + message(STATUS "Not enabling IPO / LTO on debug builds") + else() + message(STATUS "IPO / LTO not supported: <${ipo_error}>") + endif() endif() ##################################### Set Application options ##################################### From d2529177927d8ede31d2e7217c9b30ca769d87f4 Mon Sep 17 00:00:00 2001 From: Philipp David Date: Wed, 23 Mar 2022 11:40:19 +0100 Subject: [PATCH 146/605] Enable LTO for Actions --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4aa316f4..1a4e0a25 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -132,25 +132,25 @@ jobs: - name: Configure CMake if: runner.os != 'Linux' && runner.os != 'Windows' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -G Ninja - name: Configure CMake on Windows if: runner.os == 'Windows' && matrix.portable != true shell: msys2 {0} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_PORTABLE=OFF -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_PORTABLE=OFF -G Ninja - name: Configure CMake on Windows portable if: runner.os == 'Windows' && matrix.portable == true shell: msys2 {0} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -G Ninja - name: Configure CMake on Linux if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_PORTABLE=OFF -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_PORTABLE=OFF -DENABLE_LTO=ON -G Ninja - name: Build if: runner.os != 'Windows' From 51de84407f486adc8c13c4d9c2b72a75bd8adf69 Mon Sep 17 00:00:00 2001 From: Philipp David Date: Wed, 23 Mar 2022 11:36:28 +0100 Subject: [PATCH 147/605] Create vendored tarball on release --- .github/workflows/trigger_release.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index b487e731..a1d61d05 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -39,6 +39,12 @@ jobs: runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: 'true' + path: 'PolyMC-source' + - name: Download artifacts uses: actions/download-artifact@v2 @@ -49,11 +55,14 @@ jobs: - name: Package artifacts properly run: | + mv ${{ github.workspace }}/PolyMC-source PolyMC-${{ env.VERSION }} mv PolyMC-Linux*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage mv PolyMC-Windows* PolyMC-Windows-${{ env.VERSION }} mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz + tar -czf PolyMC-${{ env.VERSION }}.tar.gz PolyMC-${{ env.VERSION }} + cd PolyMC-Windows-${{ env.VERSION }} zip -r -9 ../PolyMC-Windows-${{ env.VERSION }}.zip * cd .. @@ -97,3 +106,13 @@ jobs: asset_name: PolyMC-macOS-${{ env.VERSION }}.tar.gz asset_path: PolyMC-macOS-${{ env.VERSION }}.tar.gz asset_content_type: application/gzip + + - name: Upload vendored source tarball + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_name: PolyMC-${{ env.VERSION }}.tar.gz + asset_path: PolyMC-${{ env.VERSION }}.tar.gz + asset_content_type: application/gzip From 471ea680a5aea20385667635d5a3695841bfa194 Mon Sep 17 00:00:00 2001 From: Philipp David Date: Wed, 23 Mar 2022 13:33:18 +0100 Subject: [PATCH 148/605] Update used actions and cleanup release flow --- .github/workflows/backport.yml | 4 +- .github/workflows/build.yml | 12 +-- .github/workflows/trigger_release.yml | 118 ++++++++------------------ 3 files changed, 44 insertions(+), 90 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index e1a7cffa..fa287a2c 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -9,7 +9,9 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v1 + uses: actions/checkout@v3 + with: + fetch-depth: 0 - name: Backport PR by cherry-pick-ing uses: Nathanmalnoury/gh-backport-action@master with: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4aa316f4..3e8681c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,7 +58,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: submodules: 'true' @@ -91,7 +91,7 @@ jobs: - name: Cache Qt if: runner.os != 'Windows' id: cache-qt - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: "${{ github.workspace }}/Qt/" key: ${{ runner.os }}-${{ matrix.qt_version }}-${{ matrix.qt_arch }}-qt_cache @@ -226,14 +226,14 @@ jobs: - name: Upload Linux tar.gz if: runner.os == 'Linux' && matrix.app_image != true - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC.tar.gz - name: Upload AppImage for Linux if: matrix.app_image == true - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage path: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage @@ -254,14 +254,14 @@ jobs: - name: Upload package for Windows if: runner.os == 'Windows' - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: PolyMC-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} path: ${{ env.INSTALL_DIR }}/** - name: Upload package for macOS if: runner.os == 'macOS' - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC.tar.gz diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index a1d61d05..149cb632 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -19,10 +19,36 @@ jobs: outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: 'true' + path: 'PolyMC-source' + - name: Download artifacts + uses: actions/download-artifact@v3 - name: Grab and store version run: | tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") echo "VERSION=$tag_name" >> $GITHUB_ENV + - name: Package artifacts properly + run: | + mv ${{ github.workspace }}/PolyMC-source PolyMC-${{ env.VERSION }} + mv PolyMC-Linux*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz + mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage + mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz + + tar -czf PolyMC-${{ env.VERSION }}.tar.gz PolyMC-${{ env.VERSION }} + + for d in PolyMC-Windows-*; do + cd "${d}" || continue + ARCH="$(echo -n ${d} | cut -d '-' -f 3)" + PORT="$(echo -n ${d} | grep -o portable || true)" + NAME="PolyMC-Windows-${ARCH}" + test -z "${PORT}" || NAME="${NAME}-portable" + zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * + cd .. + done + - name: Create release id: create_release uses: softprops/action-gh-release@v1 @@ -33,86 +59,12 @@ jobs: name: PolyMC ${{ env.VERSION }} draft: true prerelease: false - - upload_release: - needs: create_release - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v2 - with: - submodules: 'true' - path: 'PolyMC-source' - - - name: Download artifacts - uses: actions/download-artifact@v2 - - - name: Grab and store version - run: | - tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") - echo "VERSION=$tag_name" >> $GITHUB_ENV - - - name: Package artifacts properly - run: | - mv ${{ github.workspace }}/PolyMC-source PolyMC-${{ env.VERSION }} - mv PolyMC-Linux*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz - mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage - mv PolyMC-Windows* PolyMC-Windows-${{ env.VERSION }} - mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz - - tar -czf PolyMC-${{ env.VERSION }}.tar.gz PolyMC-${{ env.VERSION }} - - cd PolyMC-Windows-${{ env.VERSION }} - zip -r -9 ../PolyMC-Windows-${{ env.VERSION }}.zip * - cd .. - - - name: Upload Linux asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: PolyMC-Linux-${{ env.VERSION }}.tar.gz - asset_path: PolyMC-Linux-${{ env.VERSION }}.tar.gz - asset_content_type: application/gzip - - - name: Upload Linux AppImage asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage - asset_path: PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage - asset_content_type: application/x-executable - - - name: Upload Windows asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: PolyMC-Windows-${{ env.VERSION }}.zip - asset_path: PolyMC-Windows-${{ env.VERSION }}.zip - asset_content_type: application/zip - - - name: Upload macOS asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: PolyMC-macOS-${{ env.VERSION }}.tar.gz - asset_path: PolyMC-macOS-${{ env.VERSION }}.tar.gz - asset_content_type: application/gzip - - - name: Upload vendored source tarball - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: PolyMC-${{ env.VERSION }}.tar.gz - asset_path: PolyMC-${{ env.VERSION }}.tar.gz - asset_content_type: application/gzip + files: | + PolyMC-Linux-${{ env.VERSION }}.tar.gz + PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage + PolyMC-Windows-i686-${{ env.VERSION }}.zip + PolyMC-Windows-i686-portable-${{ env.VERSION }}.zip + PolyMC-Windows-x86_64-${{ env.VERSION }}.zip + PolyMC-Windows-x86_64-portable-${{ env.VERSION }}.zip + PolyMC-macOS-${{ env.VERSION }}.tar.gz + PolyMC-${{ env.VERSION }}.tar.gz From a89cbf116d37724f2bb87fe2d52afffd0775d4eb Mon Sep 17 00:00:00 2001 From: Philipp David Date: Wed, 23 Mar 2022 19:48:03 +0100 Subject: [PATCH 149/605] Allow disabling building of tests --- CMakeLists.txt | 2 +- cmake/UnitTest.cmake | 70 +++++++++++++++++++++-------------------- launcher/CMakeLists.txt | 32 ++++++++++--------- 3 files changed, 54 insertions(+), 50 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7537703c..254fce33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ if(WIN32) endif() project(Launcher) -enable_testing() +include(CTest) string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD) if(IS_IN_SOURCE_BUILD) diff --git a/cmake/UnitTest.cmake b/cmake/UnitTest.cmake index 9f2bc269..7d7bd4ad 100644 --- a/cmake/UnitTest.cmake +++ b/cmake/UnitTest.cmake @@ -5,44 +5,46 @@ set(TEST_RESOURCE_PATH ${CMAKE_CURRENT_LIST_DIR}) message(${TEST_RESOURCE_PATH}) function(add_unit_test name) - set(options "") - set(oneValueArgs DATA) - set(multiValueArgs SOURCES LIBS) + if(BUILD_TESTING) + set(options "") + set(oneValueArgs DATA) + set(multiValueArgs SOURCES LIBS) - cmake_parse_arguments(OPT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} ) + cmake_parse_arguments(OPT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} ) - if(WIN32) - add_executable(${name}_test ${OPT_SOURCES} ${TEST_RESOURCE_PATH}/UnitTest/test.rc) - else() - add_executable(${name}_test ${OPT_SOURCES}) - endif() - - if(NOT "${OPT_DATA}" STREQUAL "") - set(TEST_DATA_PATH "${CMAKE_CURRENT_BINARY_DIR}/data") - set(TEST_DATA_PATH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/${OPT_DATA}") - message("From ${TEST_DATA_PATH_SRC} to ${TEST_DATA_PATH}") - string(REGEX REPLACE "[/\\:]" "_" DATA_TARGET_NAME "${TEST_DATA_PATH_SRC}") - if(UNIX) - # on unix we get the third / from the filename - set(TEST_DATA_URL "file://${TEST_DATA_PATH}") + if(WIN32) + add_executable(${name}_test ${OPT_SOURCES} ${TEST_RESOURCE_PATH}/UnitTest/test.rc) else() - # we don't on windows, so we have to add it ourselves - set(TEST_DATA_URL "file:///${TEST_DATA_PATH}") + add_executable(${name}_test ${OPT_SOURCES}) endif() - if(NOT TARGET "${DATA_TARGET_NAME}") - add_custom_target(${DATA_TARGET_NAME}) - add_dependencies(${name}_test ${DATA_TARGET_NAME}) - add_custom_command( - TARGET ${DATA_TARGET_NAME} - COMMAND ${CMAKE_COMMAND} "-DTEST_DATA_URL=${TEST_DATA_URL}" -DSOURCE=${TEST_DATA_PATH_SRC} -DDESTINATION=${TEST_DATA_PATH} -P ${TEST_RESOURCE_PATH}/UnitTest/generate_test_data.cmake - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) + + if(NOT "${OPT_DATA}" STREQUAL "") + set(TEST_DATA_PATH "${CMAKE_CURRENT_BINARY_DIR}/data") + set(TEST_DATA_PATH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/${OPT_DATA}") + message("From ${TEST_DATA_PATH_SRC} to ${TEST_DATA_PATH}") + string(REGEX REPLACE "[/\\:]" "_" DATA_TARGET_NAME "${TEST_DATA_PATH_SRC}") + if(UNIX) + # on unix we get the third / from the filename + set(TEST_DATA_URL "file://${TEST_DATA_PATH}") + else() + # we don't on windows, so we have to add it ourselves + set(TEST_DATA_URL "file:///${TEST_DATA_PATH}") + endif() + if(NOT TARGET "${DATA_TARGET_NAME}") + add_custom_target(${DATA_TARGET_NAME}) + add_dependencies(${name}_test ${DATA_TARGET_NAME}) + add_custom_command( + TARGET ${DATA_TARGET_NAME} + COMMAND ${CMAKE_COMMAND} "-DTEST_DATA_URL=${TEST_DATA_URL}" -DSOURCE=${TEST_DATA_PATH_SRC} -DDESTINATION=${TEST_DATA_PATH} -P ${TEST_RESOURCE_PATH}/UnitTest/generate_test_data.cmake + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() endif() + + target_link_libraries(${name}_test Qt5::Test ${OPT_LIBS}) + + target_include_directories(${name}_test PRIVATE "${TEST_RESOURCE_PATH}/UnitTest/") + + add_test(NAME ${name} COMMAND ${name}_test WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) endif() - - target_link_libraries(${name}_test Qt5::Test ${OPT_LIBS}) - - target_include_directories(${name}_test PRIVATE "${TEST_RESOURCE_PATH}/UnitTest/") - - add_test(NAME ${name} COMMAND ${name}_test WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) endfunction() diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 98cb0a3b..06a822c7 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -353,21 +353,23 @@ add_unit_test(GradleSpecifier LIBS Launcher_logic ) -add_executable(PackageManifest - mojang/PackageManifest_test.cpp -) -target_link_libraries(PackageManifest - Launcher_logic - Qt5::Test -) -target_include_directories(PackageManifest - PRIVATE ../cmake/UnitTest/ -) -add_test( - NAME PackageManifest - COMMAND PackageManifest - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} -) +if(BUILD_TESTING) + add_executable(PackageManifest + mojang/PackageManifest_test.cpp + ) + target_link_libraries(PackageManifest + Launcher_logic + Qt5::Test + ) + target_include_directories(PackageManifest + PRIVATE ../cmake/UnitTest/ + ) + add_test( + NAME PackageManifest + COMMAND PackageManifest + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) +endif() add_unit_test(MojangVersionFormat SOURCES minecraft/MojangVersionFormat_test.cpp From 1b47132ebb5b825065833cdd08252466efe58faa Mon Sep 17 00:00:00 2001 From: Philipp David Date: Thu, 24 Mar 2022 08:32:26 +0100 Subject: [PATCH 150/605] libnbtplusplus: fix compilation as shared library --- libraries/libnbtplusplus | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/libnbtplusplus b/libraries/libnbtplusplus index b156bcaa..129be45a 160000 --- a/libraries/libnbtplusplus +++ b/libraries/libnbtplusplus @@ -1 +1 @@ -Subproject commit b156bcaa4acf0a5b392bbed60bd274c39e2398d4 +Subproject commit 129be45a7f91920e76673af104534d215c497d85 From eb06d0116f68307cd33f3a9391bdbd0f22559799 Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Thu, 24 Mar 2022 20:28:12 +1100 Subject: [PATCH 151/605] Merge pull request #334 from flowln/right_file_2 Fix skipping file in mod version parsing --- launcher/modplatform/modrinth/ModrinthPackIndex.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 9581ca04..992d6657 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -55,7 +55,8 @@ void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray // Find correct file (needed in cases where one version may have multiple files) // Will default to the last one if there's no primary (though I think Modrinth requires that // at least one file is primary, idk) - while (i < files.count()){ + // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed + while (i < files.count() - 1){ auto parent = files[i].toObject(); auto fileName = Json::requireString(parent, "filename"); @@ -77,6 +78,7 @@ void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack & pack, QJsonArray i++; } + auto parent = files[i].toObject(); if(parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); From a9d935f9efedf6525f0d00810e812fa7ed741bce Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 24 Mar 2022 11:15:39 +0100 Subject: [PATCH 152/605] Merge pull request #337 from oynqr/misc/create-vendored-tarball --- .github/workflows/build.yml | 12 ++-- .github/workflows/trigger_release.yml | 99 ++++++++++----------------- 2 files changed, 41 insertions(+), 70 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4aa316f4..3e8681c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,7 +58,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: submodules: 'true' @@ -91,7 +91,7 @@ jobs: - name: Cache Qt if: runner.os != 'Windows' id: cache-qt - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: "${{ github.workspace }}/Qt/" key: ${{ runner.os }}-${{ matrix.qt_version }}-${{ matrix.qt_arch }}-qt_cache @@ -226,14 +226,14 @@ jobs: - name: Upload Linux tar.gz if: runner.os == 'Linux' && matrix.app_image != true - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC.tar.gz - name: Upload AppImage for Linux if: matrix.app_image == true - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage path: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage @@ -254,14 +254,14 @@ jobs: - name: Upload package for Windows if: runner.os == 'Windows' - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: PolyMC-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} path: ${{ env.INSTALL_DIR }}/** - name: Upload package for macOS if: runner.os == 'macOS' - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC.tar.gz diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index b487e731..149cb632 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -19,10 +19,36 @@ jobs: outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: 'true' + path: 'PolyMC-source' + - name: Download artifacts + uses: actions/download-artifact@v3 - name: Grab and store version run: | tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") echo "VERSION=$tag_name" >> $GITHUB_ENV + - name: Package artifacts properly + run: | + mv ${{ github.workspace }}/PolyMC-source PolyMC-${{ env.VERSION }} + mv PolyMC-Linux*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz + mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage + mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz + + tar -czf PolyMC-${{ env.VERSION }}.tar.gz PolyMC-${{ env.VERSION }} + + for d in PolyMC-Windows-*; do + cd "${d}" || continue + ARCH="$(echo -n ${d} | cut -d '-' -f 3)" + PORT="$(echo -n ${d} | grep -o portable || true)" + NAME="PolyMC-Windows-${ARCH}" + test -z "${PORT}" || NAME="${NAME}-portable" + zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * + cd .. + done + - name: Create release id: create_release uses: softprops/action-gh-release@v1 @@ -33,67 +59,12 @@ jobs: name: PolyMC ${{ env.VERSION }} draft: true prerelease: false - - upload_release: - needs: create_release - runs-on: ubuntu-latest - steps: - - - name: Download artifacts - uses: actions/download-artifact@v2 - - - name: Grab and store version - run: | - tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") - echo "VERSION=$tag_name" >> $GITHUB_ENV - - - name: Package artifacts properly - run: | - mv PolyMC-Linux*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz - mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage - mv PolyMC-Windows* PolyMC-Windows-${{ env.VERSION }} - mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz - - cd PolyMC-Windows-${{ env.VERSION }} - zip -r -9 ../PolyMC-Windows-${{ env.VERSION }}.zip * - cd .. - - - name: Upload Linux asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: PolyMC-Linux-${{ env.VERSION }}.tar.gz - asset_path: PolyMC-Linux-${{ env.VERSION }}.tar.gz - asset_content_type: application/gzip - - - name: Upload Linux AppImage asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage - asset_path: PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage - asset_content_type: application/x-executable - - - name: Upload Windows asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: PolyMC-Windows-${{ env.VERSION }}.zip - asset_path: PolyMC-Windows-${{ env.VERSION }}.zip - asset_content_type: application/zip - - - name: Upload macOS asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_name: PolyMC-macOS-${{ env.VERSION }}.tar.gz - asset_path: PolyMC-macOS-${{ env.VERSION }}.tar.gz - asset_content_type: application/gzip + files: | + PolyMC-Linux-${{ env.VERSION }}.tar.gz + PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage + PolyMC-Windows-i686-${{ env.VERSION }}.zip + PolyMC-Windows-i686-portable-${{ env.VERSION }}.zip + PolyMC-Windows-x86_64-${{ env.VERSION }}.zip + PolyMC-Windows-x86_64-portable-${{ env.VERSION }}.zip + PolyMC-macOS-${{ env.VERSION }}.tar.gz + PolyMC-${{ env.VERSION }}.tar.gz From 82c35f27464e3fd0d0aaa4f9e495e895f8534799 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 24 Mar 2022 14:47:22 +0100 Subject: [PATCH 153/605] feat: block launch if Java is incompatible Keep track of compatible Java versions from meta. Launch-step VerifyJavaInstall will check if current instance's Java version is compatible. Also add override option both globally and per-instance in-case the user doesn't care about the requirement. --- launcher/Application.cpp | 1 + launcher/minecraft/LaunchProfile.cpp | 10 +++ launcher/minecraft/LaunchProfile.h | 5 ++ launcher/minecraft/MinecraftInstance.cpp | 1 + launcher/minecraft/MojangVersionFormat.cpp | 9 +++ launcher/minecraft/VersionFile.cpp | 1 + launcher/minecraft/VersionFile.h | 3 + .../minecraft/launch/VerifyJavaInstall.cpp | 72 ++++++++----------- launcher/minecraft/launch/VerifyJavaInstall.h | 1 + launcher/ui/pages/global/JavaPage.cpp | 2 + launcher/ui/pages/global/JavaPage.ui | 16 +++++ .../pages/instance/InstanceSettingsPage.cpp | 3 + .../ui/pages/instance/InstanceSettingsPage.ui | 10 +++ 13 files changed, 93 insertions(+), 41 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 33b1774c..0d769192 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -682,6 +682,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("JavaVendor", ""); m_settings->registerSetting("LastHostname", ""); m_settings->registerSetting("JvmArgs", ""); + m_settings->registerSetting("IgnoreJavaCompatibility", false); // Native library workarounds m_settings->registerSetting("UseNativeOpenAL", false); diff --git a/launcher/minecraft/LaunchProfile.cpp b/launcher/minecraft/LaunchProfile.cpp index 41705187..99aa642a 100644 --- a/launcher/minecraft/LaunchProfile.cpp +++ b/launcher/minecraft/LaunchProfile.cpp @@ -126,6 +126,11 @@ void LaunchProfile::applyMods(const QList& mods) } } +void LaunchProfile::applyCompatibleJavaMajors(QList& javaMajor) +{ + m_compatibleJavaMajors.append(javaMajor); +} + void LaunchProfile::applyLibrary(LibraryPtr library) { if(!library->isActive()) @@ -275,6 +280,11 @@ const QList & LaunchProfile::getMavenFiles() const return m_mavenFiles; } +const QList & LaunchProfile::getCompatibleJavaMajors() const +{ + return m_compatibleJavaMajors; +} + void LaunchProfile::getLibraryFiles( const QString& architecture, QStringList& jars, diff --git a/launcher/minecraft/LaunchProfile.h b/launcher/minecraft/LaunchProfile.h index c1752531..48aec83b 100644 --- a/launcher/minecraft/LaunchProfile.h +++ b/launcher/minecraft/LaunchProfile.h @@ -21,6 +21,7 @@ public: /* application of profile variables from patches */ void applyMods(const QList &jarMods); void applyLibrary(LibraryPtr library); void applyMavenFile(LibraryPtr library); + void applyCompatibleJavaMajors(QList& javaMajor); void applyMainJar(LibraryPtr jar); void applyProblemSeverity(ProblemSeverity severity); /// clear the profile @@ -39,6 +40,7 @@ public: /* getters for profile variables */ const QList & getLibraries() const; const QList & getNativeLibraries() const; const QList & getMavenFiles() const; + const QList & getCompatibleJavaMajors() const; const LibraryPtr getMainJar() const; void getLibraryFiles( const QString & architecture, @@ -99,6 +101,9 @@ private: /// the list of mods QList m_mods; + /// compatible java major versions + QList m_compatibleJavaMajors; + ProblemSeverity m_problemSeverity = ProblemSeverity::None; }; diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 6db12c42..cd858a86 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -88,6 +88,7 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO m_settings->registerOverride(globalSettings->getSetting("JavaPath"), javaOrLocation); m_settings->registerOverride(globalSettings->getSetting("JvmArgs"), javaOrArgs); + m_settings->registerOverride(globalSettings->getSetting("IgnoreJavaCompatibility"), javaOrLocation); // special! m_settings->registerPassthrough(globalSettings->getSetting("JavaTimestamp"), javaOrLocation); diff --git a/launcher/minecraft/MojangVersionFormat.cpp b/launcher/minecraft/MojangVersionFormat.cpp index ff5409fd..d0bf7fc5 100644 --- a/launcher/minecraft/MojangVersionFormat.cpp +++ b/launcher/minecraft/MojangVersionFormat.cpp @@ -183,6 +183,15 @@ void MojangVersionFormat::readVersionProperties(const QJsonObject &in, VersionFi ); } } + + if (in.contains("compatibleJavaMajors")) + { + for (auto compatible : requireArray(in.value("compatibleJavaMajors"))) + { + out->compatibleJavaMajors.append(requireInteger(compatible)); + } + } + if(in.contains("downloads")) { auto downloadsObj = requireObject(in, "downloads"); diff --git a/launcher/minecraft/VersionFile.cpp b/launcher/minecraft/VersionFile.cpp index d0a1a507..c6018d05 100644 --- a/launcher/minecraft/VersionFile.cpp +++ b/launcher/minecraft/VersionFile.cpp @@ -36,6 +36,7 @@ void VersionFile::applyTo(LaunchProfile *profile) profile->applyJarMods(jarMods); profile->applyMods(mods); profile->applyTraits(traits); + profile->applyCompatibleJavaMajors(compatibleJavaMajors); for (auto library : libraries) { diff --git a/launcher/minecraft/VersionFile.h b/launcher/minecraft/VersionFile.h index 239a4069..e0082abe 100644 --- a/launcher/minecraft/VersionFile.h +++ b/launcher/minecraft/VersionFile.h @@ -57,6 +57,9 @@ public: /* data */ /// Mojang: Minecraft launch arguments (may contain placeholders for variable substitution) QString minecraftArguments; + /// Mojang: list of compatible java majors + QList compatibleJavaMajors; + /// Mojang: type of the Minecraft version QString type; diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp index 15acf678..c1508e52 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.cpp +++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -1,50 +1,40 @@ #include "VerifyJavaInstall.h" -#include -#include -#include -#include - -#ifdef major - #undef major -#endif -#ifdef minor - #undef minor -#endif +#include "java/JavaVersion.h" +#include "minecraft/PackProfile.h" +#include "minecraft/MinecraftInstance.h" void VerifyJavaInstall::executeTask() { - auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + auto instance = std::dynamic_pointer_cast(m_parent->instance()); + auto packProfile = instance->getPackProfile(); + auto settings = instance->settings(); + auto storedVersion = settings->get("JavaVersion").toString(); + auto ignoreCompatibility = settings->get("IgnoreJavaCompatibility").toBool(); - auto javaVersion = m_inst->getJavaVersion(); - auto minecraftComponent = m_inst->getPackProfile()->getComponent("net.minecraft"); + auto compatibleMajors = packProfile->getProfile()->getCompatibleJavaMajors(); - // Java 17 requirement - if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java17BeginsDate) { - if (javaVersion.major() < 17) { - emit logLine("Minecraft 1.18 Pre Release 2 and above require the use of Java 17", - MessageLevel::Fatal); - emitFailed(tr("Minecraft 1.18 Pre Release 2 and above require the use of Java 17")); - return; - } - } - // Java 16 requirement - else if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java16BeginsDate) { - if (javaVersion.major() < 16) { - emit logLine("Minecraft 21w19a and above require the use of Java 16", - MessageLevel::Fatal); - emitFailed(tr("Minecraft 21w19a and above require the use of Java 16")); - return; - } - } - // Java 8 requirement - else if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java8BeginsDate) { - if (javaVersion.major() < 8) { - emit logLine("Minecraft 17w13a and above require the use of Java 8", - MessageLevel::Fatal); - emitFailed(tr("Minecraft 17w13a and above require the use of Java 8")); - return; - } + JavaVersion javaVersion(storedVersion); + + if (compatibleMajors.isEmpty() || compatibleMajors.contains(javaVersion.major())) + { + emitSucceeded(); + return; } - emitSucceeded(); + + if (ignoreCompatibility) + { + emit logLine(tr("Java major version is incompatible. Things might break."), MessageLevel::Warning); + emitSucceeded(); + return; + } + + emit logLine(tr("Instance not compatible with Java major version %1.\n" + "Switch the Java version of this instance to one of the following:").arg(javaVersion.major()), + MessageLevel::Error); + for (auto major: compatibleMajors) + { + emit logLine(tr("Java %1").arg(major), MessageLevel::Error); + } + emitFailed(QString("Incompatible Java major version")); } diff --git a/launcher/minecraft/launch/VerifyJavaInstall.h b/launcher/minecraft/launch/VerifyJavaInstall.h index a553106d..182ee715 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.h +++ b/launcher/minecraft/launch/VerifyJavaInstall.h @@ -1,6 +1,7 @@ #pragma once #include +#include class VerifyJavaInstall : public LaunchStep { Q_OBJECT diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index 3eb4bd59..d1c61287 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -95,6 +95,7 @@ void JavaPage::applySettings() // Java Settings s->set("JavaPath", ui->javaPathTextBox->text()); s->set("JvmArgs", ui->jvmArgsTextBox->text()); + s->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked()); JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget()); } void JavaPage::loadSettings() @@ -118,6 +119,7 @@ void JavaPage::loadSettings() // Java Settings ui->javaPathTextBox->setText(s->get("JavaPath").toString()); ui->jvmArgsTextBox->setText(s->get("JvmArgs").toString()); + ui->skipCompatibilityCheckbox->setChecked(s->get("IgnoreJavaCompatibility").toBool()); } void JavaPage::on_javaDetectBtn_clicked() diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index b67e9994..d27b200f 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -222,6 +222,22 @@
+ + + + + 0 + 0 + + + + If enabled, the launcher will not check if an instance is compatible with the selected Java version. + + + Skip Java compatibility checks + + +
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index e68a7124..a5985741 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -165,10 +165,12 @@ void InstanceSettingsPage::applySettings() 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 @@ -286,6 +288,7 @@ void InstanceSettingsPage::loadSettings() ui->javaSettingsGroupBox->setChecked(overrideLocation); 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()); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index 729f8e2a..5db2d147 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -85,6 +85,16 @@
+ + + + If enabled, the launcher will not check if an instance is compatible with the selected Java version. + + + Skip Java compatibility checks + + +
From e02369ba6bfd061dd92b63a19957a412a5ffcb81 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 24 Mar 2022 16:00:23 +0100 Subject: [PATCH 154/605] chore: add license header --- launcher/minecraft/LaunchProfile.cpp | 35 +++++++++++++++++++ launcher/minecraft/LaunchProfile.h | 35 +++++++++++++++++++ launcher/minecraft/MinecraftInstance.cpp | 35 +++++++++++++++++++ launcher/minecraft/MojangVersionFormat.cpp | 35 +++++++++++++++++++ launcher/minecraft/VersionFile.cpp | 35 +++++++++++++++++++ launcher/minecraft/VersionFile.h | 35 +++++++++++++++++++ .../minecraft/launch/VerifyJavaInstall.cpp | 35 +++++++++++++++++++ launcher/minecraft/launch/VerifyJavaInstall.h | 35 +++++++++++++++++++ launcher/ui/pages/global/JavaPage.cpp | 1 + 9 files changed, 281 insertions(+) diff --git a/launcher/minecraft/LaunchProfile.cpp b/launcher/minecraft/LaunchProfile.cpp index 99aa642a..cd77aa4a 100644 --- a/launcher/minecraft/LaunchProfile.cpp +++ b/launcher/minecraft/LaunchProfile.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "LaunchProfile.h" #include diff --git a/launcher/minecraft/LaunchProfile.h b/launcher/minecraft/LaunchProfile.h index 48aec83b..366ed805 100644 --- a/launcher/minecraft/LaunchProfile.h +++ b/launcher/minecraft/LaunchProfile.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "Library.h" diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index cd858a86..7c289300 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "MinecraftInstance.h" #include "minecraft/launch/CreateGameFolders.h" #include "minecraft/launch/ExtractNatives.h" diff --git a/launcher/minecraft/MojangVersionFormat.cpp b/launcher/minecraft/MojangVersionFormat.cpp index d0bf7fc5..94c58676 100644 --- a/launcher/minecraft/MojangVersionFormat.cpp +++ b/launcher/minecraft/MojangVersionFormat.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "MojangVersionFormat.h" #include "OneSixVersionFormat.h" #include "MojangDownloadInfo.h" diff --git a/launcher/minecraft/VersionFile.cpp b/launcher/minecraft/VersionFile.cpp index c6018d05..94fb6db7 100644 --- a/launcher/minecraft/VersionFile.cpp +++ b/launcher/minecraft/VersionFile.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 diff --git a/launcher/minecraft/VersionFile.h b/launcher/minecraft/VersionFile.h index e0082abe..a7a19c4e 100644 --- a/launcher/minecraft/VersionFile.h +++ b/launcher/minecraft/VersionFile.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp index c1508e52..42754d44 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.cpp +++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "VerifyJavaInstall.h" #include "java/JavaVersion.h" diff --git a/launcher/minecraft/launch/VerifyJavaInstall.h b/launcher/minecraft/launch/VerifyJavaInstall.h index 182ee715..9139c0fa 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.h +++ b/launcher/minecraft/launch/VerifyJavaInstall.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index d1c61287..f0616db1 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by From d00c320c0041421e67d5d8ec6deb4427d0f8020c Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 24 Mar 2022 18:39:53 -0300 Subject: [PATCH 155/605] optimize: Improve mod versions request to Modrinth This uses more arguments in the GET request for mod versions on the Modrinth API, filtering what versions can be returned, decreasing load on Modrinth servers and improving a little the time it takes for the versions to be available to the user. This also removes the now unneeded check on correct modloaders in ModrinthPackIndex, since it is now filtered by the Modrinth server. Lastly, this adds a couple of helper functions in ModModel. --- launcher/modplatform/ModAPI.h | 11 ++++++- launcher/modplatform/flame/FlameAPI.h | 4 +-- .../modplatform/helpers/NetworkModAPI.cpp | 10 +++--- launcher/modplatform/helpers/NetworkModAPI.h | 4 +-- launcher/modplatform/modrinth/ModrinthAPI.h | 20 +++++++++-- .../modrinth/ModrinthPackIndex.cpp | 12 ------- launcher/ui/pages/modplatform/ModModel.cpp | 33 ++++++++++++------- launcher/ui/pages/modplatform/ModModel.h | 3 ++ 8 files changed, 61 insertions(+), 36 deletions(-) diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index 5c7c6349..ae6ac80f 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace ModPlatform { class ListModel; @@ -25,5 +26,13 @@ class ModAPI { }; virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0; - virtual void getVersions(CallerType* caller, const QString& addonId) const = 0; + + + struct VersionSearchArgs { + QString addonId; + QList mcVersions; + ModLoaderType loader; + }; + + virtual void getVersions(CallerType* caller, VersionSearchArgs&& args) const = 0; }; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 62accfa4..8654a693 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -25,8 +25,8 @@ class FlameAPI : public NetworkModAPI { .arg(args.version); }; - inline auto getVersionsURL(const QString& addonId) const -> QString override + inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { - return QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId); + return QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(args.addonId); }; }; diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp index 25c7b9fd..6829b837 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.cpp +++ b/launcher/modplatform/helpers/NetworkModAPI.cpp @@ -31,14 +31,14 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const netJob->start(); } -void NetworkModAPI::getVersions(CallerType* caller, const QString& addonId) const +void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) const { - auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(caller->debugName()).arg(addonId), APPLICATION->network()); + auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(caller->debugName()).arg(args.addonId), APPLICATION->network()); auto response = new QByteArray(); - netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(addonId), response)); + netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response)); - QObject::connect(netJob, &NetJob::succeeded, caller, [response, caller, addonId] { + QObject::connect(netJob, &NetJob::succeeded, caller, [response, caller, args] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -48,7 +48,7 @@ void NetworkModAPI::getVersions(CallerType* caller, const QString& addonId) cons return; } - caller->versionRequestSucceeded(doc, addonId); + caller->versionRequestSucceeded(doc, args.addonId); }); QObject::connect(netJob, &NetJob::finished, caller, [response, netJob] { diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h index 4d3f7005..000620b2 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.h +++ b/launcher/modplatform/helpers/NetworkModAPI.h @@ -5,9 +5,9 @@ class NetworkModAPI : public ModAPI { public: void searchMods(CallerType* caller, SearchArgs&& args) const override; - void getVersions(CallerType* caller, const QString& addonId) const override; + void getVersions(CallerType* caller, VersionSearchArgs&& args) const override; protected: virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0; - virtual auto getVersionsURL(const QString& addonId) const -> QString = 0; + virtual auto getVersionsURL(VersionSearchArgs& args) const -> QString = 0; }; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index cf4dec1a..30952e99 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -30,11 +30,27 @@ class ModrinthAPI : public NetworkModAPI { .arg(args.version); }; - inline auto getVersionsURL(const QString& addonId) const -> QString override + inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { - return QString("https://api.modrinth.com/v2/project/%1/version").arg(addonId); + return QString("https://api.modrinth.com/v2/project/%1/version?" + "game_versions=[%2]" + "loaders=[%3]") + .arg(args.addonId) + .arg(getGameVersionsString(args.mcVersions)) + .arg(getModLoaderString(args.loader)); }; + inline auto getGameVersionsString(QList mcVersions) const -> QString + { + QString s; + for(int i = 0; i < mcVersions.count(); i++){ + s += mcVersions.at(i); + if(i < mcVersions.count() - 1) + s += ","; + } + return s; + } + inline auto getModLoaderString(ModLoaderType modLoader) const -> QString { switch (modLoader) { diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 82988cf6..a4e56d4f 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -30,7 +30,6 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, BaseInstance* inst) { QVector unsortedVersions; - bool hasFabric = !(static_cast(inst))->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); QString mcVersion = (static_cast(inst))->getPackProfile()->getComponentVersion("net.minecraft"); for (auto versionIter : arr) { @@ -61,17 +60,6 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, auto parent = files[i].toObject(); auto fileName = Json::requireString(parent, "filename"); - // Grab the correct mod loader - if(hasFabric){ - if(fileName.contains("forge",Qt::CaseInsensitive)){ - i++; - continue; - } - } else if(fileName.contains("fabric", Qt::CaseInsensitive)){ - i++; - continue; - } - // Grab the primary file, if available if(Json::requireBoolean(parent, "primary")) break; diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index cc3c5326..0a147584 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -16,7 +16,6 @@ auto ListModel::debugName() const -> QString return m_parent->debugName(); } - /******** Make data requests ********/ void ListModel::fetchMore(const QModelIndex& parent) @@ -61,19 +60,14 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) { - m_parent->apiProvider()->getVersions(this, current.addonId.toString()); + m_parent->apiProvider()->getVersions(this, + { current.addonId.toString(), getMineVersions(), hasFabric() ? ModAPI::ModLoaderType::Fabric : ModAPI::ModLoaderType::Forge }); } void ListModel::performPaginatedSearch() { - QString mcVersion = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile()->getComponentVersion("net.minecraft"); - bool hasFabric = !(dynamic_cast((dynamic_cast(parent()))->m_instance)) - ->getPackProfile() - ->getComponentVersion("net.fabricmc.fabric-loader") - .isEmpty(); - - m_parent->apiProvider()->searchMods( - this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], hasFabric ? ModAPI::Fabric : ModAPI::Forge, mcVersion }); + m_parent->apiProvider()->searchMods(this, + { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], hasFabric() ? ModAPI::Fabric : ModAPI::Forge, getMineVersions().at(0) }); } void ListModel::searchWithTerm(const QString& term, const int sort) @@ -131,7 +125,6 @@ void ListModel::requestLogo(QString logo, QString url) m_loadingLogos.append(logo); } - /******** Request callbacks ********/ void ListModel::logoLoaded(QString logo, QIcon out) @@ -208,7 +201,7 @@ void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) { auto& current = m_parent->getCurrent(); if (addonId != current.addonId) { return; } - + QJsonArray arr = doc.array(); try { loadIndexedPackVersions(current, arr); @@ -221,3 +214,19 @@ void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) } } // namespace ModPlatform + +/******** Helpers ********/ +auto ModPlatform::ListModel::hasFabric() const -> bool +{ + return !(dynamic_cast((dynamic_cast(parent()))->m_instance)) + ->getPackProfile() + ->getComponentVersion("net.fabricmc.fabric-loader") + .isEmpty(); +} + +auto ModPlatform::ListModel::getMineVersions() const -> QList +{ + return { (dynamic_cast((dynamic_cast(parent()))->m_instance)) + ->getPackProfile() + ->getComponentVersion("net.minecraft") }; +} diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 02be6049..64cfa71e 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -62,6 +62,9 @@ class ListModel : public QAbstractListModel { void requestLogo(QString file, QString url); + inline auto hasFabric() const -> bool; + inline auto getMineVersions() const -> QList; + protected: ModPage* m_parent; From 75301bec4b7a40918fc7d7e4564503703b5ca7bf Mon Sep 17 00:00:00 2001 From: txtsd Date: Fri, 25 Mar 2022 22:41:38 +0530 Subject: [PATCH 156/605] fix(templates): Use correct labels --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/rfc.yml | 2 +- .github/ISSUE_TEMPLATE/suggestion.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index eb560f0e..953977ad 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,6 @@ name: Bug Report description: File a bug report -labels: [bug, needs-triage] +labels: [bug] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/rfc.yml b/.github/ISSUE_TEMPLATE/rfc.yml index 664430fe..1ea254ed 100644 --- a/.github/ISSUE_TEMPLATE/rfc.yml +++ b/.github/ISSUE_TEMPLATE/rfc.yml @@ -1,7 +1,7 @@ # Template based on https://gitlab.archlinux.org/archlinux/rfcs/-/blob/0ba3b61e987e197f8d1901709409b8564958f78a/rfcs/0000-template.rst name: Request for Comment (RFC) description: Propose a larger change and start a discussion. -labels: [RFC] +labels: [rfc] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/suggestion.yml b/.github/ISSUE_TEMPLATE/suggestion.yml index b58a6672..48f157b3 100644 --- a/.github/ISSUE_TEMPLATE/suggestion.yml +++ b/.github/ISSUE_TEMPLATE/suggestion.yml @@ -1,6 +1,6 @@ name: Suggestion description: Make a suggestion -labels: [idea, needs-triage] +labels: [enhancement] body: - type: markdown attributes: From cb384261b8c157711fdad664877b2ff452098a99 Mon Sep 17 00:00:00 2001 From: txtsd Date: Fri, 25 Mar 2022 22:42:41 +0530 Subject: [PATCH 157/605] fix(templates): Replace dead FAQ link with new wiki link --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 953977ad..1ede3f74 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,7 +8,7 @@ body: 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 [FAQ](https://github.com/PolyMC/PolyMC/wiki/FAQ) and it has not answered your question + * You have read the [PolyMC wiki](https://polymc.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/PolyMC/PolyMC/issues) From 6bd345b1ade8d4a83cf9250355bcbc801e80c679 Mon Sep 17 00:00:00 2001 From: txtsd Date: Fri, 25 Mar 2022 22:43:26 +0530 Subject: [PATCH 158/605] fix(templates): Unsplit bulleted line --- .github/ISSUE_TEMPLATE/rfc.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/rfc.yml b/.github/ISSUE_TEMPLATE/rfc.yml index 1ea254ed..0a40d01d 100644 --- a/.github/ISSUE_TEMPLATE/rfc.yml +++ b/.github/ISSUE_TEMPLATE/rfc.yml @@ -21,8 +21,7 @@ body: Introduce the topic. If this is a not-well-known section of PolyMC, 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!). + - 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: From 0d46ea5c71a7c1cb06fc36a530a4036ed997feb2 Mon Sep 17 00:00:00 2001 From: txtsd Date: Fri, 25 Mar 2022 23:17:14 +0530 Subject: [PATCH 159/605] chore: Ignore more paths --- .github/workflows/trigger_builds.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 1561b9d6..7cf10b1a 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -9,12 +9,16 @@ on: - '**/LICENSE' - 'flake.lock' - '**.nix' + - 'packages/**' + - '.github/ISSUE_TEMPLATE/**' pull_request: paths-ignore: - '**.md' - '**/LICENSE' - 'flake.lock' - '**.nix' + - 'packages/**' + - '.github/ISSUE_TEMPLATE/**' workflow_dispatch: jobs: From 94e7961df018bbc00fbb06786ca3488b9490572a Mon Sep 17 00:00:00 2001 From: txtsd Date: Fri, 25 Mar 2022 23:18:55 +0530 Subject: [PATCH 160/605] chore: Don't build release type during development --- .github/workflows/trigger_builds.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 7cf10b1a..3ec6bb95 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -28,9 +28,3 @@ jobs: uses: ./.github/workflows/build.yml with: build_type: Debug - - build_release: - name: Build Release - uses: ./.github/workflows/build.yml - with: - build_type: Release From 54d2c91320e446776ccc36d13c171168676222fb Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Fri, 25 Mar 2022 13:13:50 +0100 Subject: [PATCH 161/605] bring back portable linux builds --- .github/workflows/build.yml | 38 ++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e8681c9..b011a779 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,12 @@ jobs: qt_version: 5.12.8 qt_host: linux + - os: ubuntu-20.04 + name: Linux-Portable + qt_version: 5.12.8 + qt_host: linux + portable: true + - os: ubuntu-20.04 qt_version: 5.15.2 qt_host: linux @@ -140,7 +146,6 @@ jobs: run: | cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_PORTABLE=OFF -G Ninja - - name: Configure CMake on Windows portable if: runner.os == 'Windows' && matrix.portable == true shell: msys2 {0} @@ -148,10 +153,15 @@ jobs: cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja - name: Configure CMake on Linux - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.portable != true run: | cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_PORTABLE=OFF -G Ninja + - name: Configure CMake on Linux Portable + if: runner.os == 'Linux' && matrix.portable == true + run: | + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja + - name: Build if: runner.os != 'Windows' run: | @@ -175,10 +185,15 @@ jobs: cmake --install ${{ env.BUILD_DIR }} - name: Install on Linux - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.portable != true run: | DESTDIR=${{ env.INSTALL_DIR }} cmake --install ${{ env.BUILD_DIR }} + - name: Install on Linux portable + if: runner.os == 'Linux' && matrix.portable == true + run: | + cmake --install ${{ env.BUILD_DIR }} + - name: Bundle AppImage if: matrix.app_image == true shell: bash @@ -219,18 +234,31 @@ jobs: tar -czf ../PolyMC.tar.gz * - name: tar on Linux - if: runner.os == 'Linux' && matrix.app_image != true + if: runner.os == 'Linux' && matrix.app_image != true && matrix.portable != true run: | cd ${{ env.INSTALL_DIR }} tar -czf ../PolyMC.tar.gz * + - name: tar on Linux portable + if: runner.os == 'Linux' && matrix.app_image != true && matrix.portable == true + run: | + cd ${{ env.INSTALL_DIR }} + tar -czf ../PolyMC-portable.tar.gz * + - name: Upload Linux tar.gz - if: runner.os == 'Linux' && matrix.app_image != true + if: runner.os == 'Linux' && matrix.app_image != true && matrix.portable != true uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC.tar.gz + - name: Upload Linux Portable tar.gz + if: runner.os == 'Linux' && matrix.app_image != true && matrix.portable == true + uses: actions/upload-artifact@v3 + with: + name: PolyMC-${{ runner.os }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} + path: PolyMC-portable.tar.gz + - name: Upload AppImage for Linux if: matrix.app_image == true uses: actions/upload-artifact@v3 From bd60c5491109711c7812bc28b5edef61d0e1210b Mon Sep 17 00:00:00 2001 From: LennyMcLennington Date: Sat, 26 Mar 2022 09:55:06 +0000 Subject: [PATCH 162/605] Merge pull request #314 from Scrumplex/chore-bump-1.1.1 Bump to 1.1.1 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 12ff771b..b41dd076 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,7 +53,7 @@ set(Launcher_HELP_URL "https://polymc.org/wiki/%1" CACHE STRING "URL (with arg % ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 1) set(Launcher_VERSION_MINOR 1) -set(Launcher_VERSION_HOTFIX 0) +set(Launcher_VERSION_HOTFIX 1) # Build number set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") From 3672dbc5af1403c8ea1a1f36d2890fed7dd4c6a1 Mon Sep 17 00:00:00 2001 From: dada513 Date: Sun, 27 Mar 2022 12:43:49 +0200 Subject: [PATCH 163/605] Fix flatpak properly --- launcher/CMakeLists.txt | 2 ++ launcher/DesktopServices.cpp | 48 +++++++++++++++++++++++++++++++----- launcher/Flatpak.cpp | 14 +++++++++++ launcher/Flatpak.h | 4 +++ 4 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 launcher/Flatpak.cpp create mode 100644 launcher/Flatpak.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 98cb0a3b..1a16485e 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -582,6 +582,8 @@ SET(LAUNCHER_SOURCES ApplicationMessage.cpp # GUI - general utilities + Flatpak.h + Flatpak.cpp DesktopServices.h DesktopServices.cpp VersionProxyModel.h diff --git a/launcher/DesktopServices.cpp b/launcher/DesktopServices.cpp index dcc1b0ce..5de97210 100644 --- a/launcher/DesktopServices.cpp +++ b/launcher/DesktopServices.cpp @@ -3,6 +3,7 @@ #include #include #include +#include "Flatpak.h" /** * This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing. @@ -84,7 +85,14 @@ bool openDirectory(const QString &path, bool ensureExists) return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath())); }; #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - return IndirectOpen(f); + if(!Flatpak::IsFlatpak()) + { + return IndirectOpen(f); + } + else + { + return f(); + } #else return f(); #endif @@ -98,7 +106,14 @@ bool openFile(const QString &path) return QDesktopServices::openUrl(QUrl::fromLocalFile(path)); }; #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - return IndirectOpen(f); + if(!Flatpak::IsFlatpak()) + { + return IndirectOpen(f); + } + else + { + return f(); + } #else return f(); #endif @@ -109,10 +124,17 @@ bool openFile(const QString &application, const QString &path, const QString &wo qDebug() << "Opening file" << path << "using" << application; #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) // FIXME: the pid here is fake. So if something depends on it, it will likely misbehave - return IndirectOpen([&]() + if(!Flatpak::IsFlatpak()) { - return QProcess::startDetached(application, QStringList() << path, workingDirectory); - }, pid); + return IndirectOpen([&]() + { + return QProcess::startDetached(application, QStringList() << path, workingDirectory); + }, pid); + } + else + { + return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid); + } #else return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid); #endif @@ -122,11 +144,18 @@ bool run(const QString &application, const QStringList &args, const QString &wor { qDebug() << "Running" << application << "with args" << args.join(' '); #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + if(!Flatpak::IsFlatpak()) + { // FIXME: the pid here is fake. So if something depends on it, it will likely misbehave return IndirectOpen([&]() { return QProcess::startDetached(application, args, workingDirectory); }, pid); + } + else + { + return QProcess::startDetached(application, args, workingDirectory, pid); + } #else return QProcess::startDetached(application, args, workingDirectory, pid); #endif @@ -140,7 +169,14 @@ bool openUrl(const QUrl &url) return QDesktopServices::openUrl(url); }; #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - return IndirectOpen(f); + if(!Flatpak::IsFlatpak()) + { + return IndirectOpen(f); + } + else + { + return f(); + } #else return f(); #endif diff --git a/launcher/Flatpak.cpp b/launcher/Flatpak.cpp new file mode 100644 index 00000000..8b61f903 --- /dev/null +++ b/launcher/Flatpak.cpp @@ -0,0 +1,14 @@ +#include + +namespace Flatpak +{ + bool IsFlatpak() + { + #ifdef Q_OS_LINUX + QFileInfo check_file("/.flatpak-info"); + return check_file.exists(); + #else + return false; + #endif + } +} diff --git a/launcher/Flatpak.h b/launcher/Flatpak.h new file mode 100644 index 00000000..da6eb7c3 --- /dev/null +++ b/launcher/Flatpak.h @@ -0,0 +1,4 @@ +namespace Flatpak +{ + bool IsFlatpak(); +} \ No newline at end of file From 6a180f495f28db0ae254a17112618291369e86a2 Mon Sep 17 00:00:00 2001 From: dada513 Date: Sun, 27 Mar 2022 14:42:02 +0200 Subject: [PATCH 164/605] more flatpak fixes --- .gitignore | 4 ++++ launcher/DesktopServices.cpp | 34 ++++++++++++++++++++++++++++++++++ launcher/Flatpak.cpp | 19 ++++++++++++++++++- launcher/Flatpak.h | 17 +++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ba90e8f8..dc4cd1c5 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,7 @@ run/ # Nix/NixOS result/ + +# Flatpak +.flatpak-builder +flatbuild \ No newline at end of file diff --git a/launcher/DesktopServices.cpp b/launcher/DesktopServices.cpp index 5de97210..f4226c15 100644 --- a/launcher/DesktopServices.cpp +++ b/launcher/DesktopServices.cpp @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 dada513 + * + * 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-2022 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 "DesktopServices.h" #include #include diff --git a/launcher/Flatpak.cpp b/launcher/Flatpak.cpp index 8b61f903..2c28903c 100644 --- a/launcher/Flatpak.cpp +++ b/launcher/Flatpak.cpp @@ -1,3 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 dada513 + * + * 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 namespace Flatpak @@ -10,5 +27,5 @@ namespace Flatpak #else return false; #endif - } + } } diff --git a/launcher/Flatpak.h b/launcher/Flatpak.h index da6eb7c3..50c6d734 100644 --- a/launcher/Flatpak.h +++ b/launcher/Flatpak.h @@ -1,3 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 dada513 + * + * 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 . + */ namespace Flatpak { bool IsFlatpak(); From 0a5dfeb3d7767a024f3b9032ea84df82570c7052 Mon Sep 17 00:00:00 2001 From: dada513 Date: Sun, 27 Mar 2022 14:44:40 +0200 Subject: [PATCH 165/605] fix newline (scrumplex nitpick not allowed) --- launcher/Flatpak.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/Flatpak.h b/launcher/Flatpak.h index 50c6d734..8fd315fb 100644 --- a/launcher/Flatpak.h +++ b/launcher/Flatpak.h @@ -18,4 +18,4 @@ namespace Flatpak { bool IsFlatpak(); -} \ No newline at end of file +} From b1af689546704d5e05ef4bc44f43ce9e8bb4854c Mon Sep 17 00:00:00 2001 From: dada513 Date: Wed, 23 Mar 2022 19:06:17 +0100 Subject: [PATCH 166/605] Add quit launcher after game stops option (Steam Deck) lecense --- launcher/Application.cpp | 1 + launcher/CMakeLists.txt | 2 ++ launcher/launch/steps/QuitAfterGameStop.cpp | 26 ++++++++++++++ launcher/launch/steps/QuitAfterGameStop.h | 35 +++++++++++++++++++ launcher/minecraft/MinecraftInstance.cpp | 6 ++++ .../minecraft/launch/LauncherPartLaunch.cpp | 1 + launcher/ui/pages/global/MinecraftPage.cpp | 2 ++ launcher/ui/pages/global/MinecraftPage.ui | 10 ++++++ 8 files changed, 83 insertions(+) create mode 100644 launcher/launch/steps/QuitAfterGameStop.cpp create mode 100644 launcher/launch/steps/QuitAfterGameStop.h diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 33b1774c..e701acca 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -727,6 +727,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("PastebinURL", "https://0x0.st"); m_settings->registerSetting("CloseAfterLaunch", false); + m_settings->registerSetting("QuitAfterGameStop", false); // Custom MSA credentials m_settings->registerSetting("MSAClientIDOverride", ""); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 98cb0a3b..c65c17ce 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -144,6 +144,8 @@ set(LAUNCH_SOURCES launch/steps/TextPrint.h launch/steps/Update.cpp launch/steps/Update.h + launch/steps/QuitAfterGameStop.cpp + launch/steps/QuitAfterGameStop.h launch/LaunchStep.cpp launch/LaunchStep.h launch/LaunchTask.cpp diff --git a/launcher/launch/steps/QuitAfterGameStop.cpp b/launcher/launch/steps/QuitAfterGameStop.cpp new file mode 100644 index 00000000..257f473d --- /dev/null +++ b/launcher/launch/steps/QuitAfterGameStop.cpp @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 dada513 + * + * 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 "QuitAfterGameStop.h" +#include +#include "Application.h" + +void QuitAfterGameStop::executeTask() +{ + APPLICATION->quit(); +} \ No newline at end of file diff --git a/launcher/launch/steps/QuitAfterGameStop.h b/launcher/launch/steps/QuitAfterGameStop.h new file mode 100644 index 00000000..1ce14da9 --- /dev/null +++ b/launcher/launch/steps/QuitAfterGameStop.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 dada513 + * + * 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 + +class QuitAfterGameStop: public LaunchStep +{ + Q_OBJECT +public: + explicit QuitAfterGameStop(LaunchTask *parent) :LaunchStep(parent){}; + virtual ~QuitAfterGameStop() {}; + + virtual void executeTask(); + virtual bool canAbort() const + { + return false; + } +}; diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 6db12c42..90bb92a1 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -20,6 +20,7 @@ #include "launch/steps/PreLaunchCommand.h" #include "launch/steps/TextPrint.h" #include "launch/steps/CheckJava.h" +#include "launch/steps/QuitAfterGameStop.h" #include "minecraft/launch/LauncherPartLaunch.h" #include "minecraft/launch/DirectJavaLaunch.h" @@ -935,6 +936,11 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt { process->setCensorFilter(createCensorFilterFromSession(session)); } + if(APPLICATION->settings()->get("QuitAfterGameStop").toBool()) + { + auto step = new QuitAfterGameStop(pptr); + process->appendStep(step); + } m_launchProcess = process; emit launchTaskChanged(m_launchProcess); return m_launchProcess; diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index d15d7e9d..173f29b5 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -170,6 +170,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) { if (APPLICATION->settings()->get("CloseAfterLaunch").toBool()) APPLICATION->showMainWindow(); + m_parent->setPid(-1); // if the exit code wasn't 0, report this as a crash auto exitCode = m_process.exitCode(); diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index 9abae425..f49f5a92 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -94,6 +94,7 @@ void MinecraftPage::applySettings() // Miscellaneous s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); + s->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked()); } void MinecraftPage::loadSettings() @@ -113,6 +114,7 @@ void MinecraftPage::loadSettings() ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool()); ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool()); + ui->quitAfterGameStopCheck->setChecked(s->get("QuitAfterGameStop").toBool()); } void MinecraftPage::retranslate() diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index a28b1f59..decc9b8b 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -180,6 +180,16 @@
+ + + + <html><head/><body><p>PolyMC will automatically exit if the game crashes or exists.</p></body></html> + + + Quit PolyMC after game window stops + + + From ec6409914d07dce67f86522ff7f5015e5060fb7e Mon Sep 17 00:00:00 2001 From: dada513 Date: Sun, 27 Mar 2022 14:51:48 +0200 Subject: [PATCH 167/605] newline more like waste --- launcher/launch/steps/QuitAfterGameStop.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/launch/steps/QuitAfterGameStop.cpp b/launcher/launch/steps/QuitAfterGameStop.cpp index 257f473d..f9eced99 100644 --- a/launcher/launch/steps/QuitAfterGameStop.cpp +++ b/launcher/launch/steps/QuitAfterGameStop.cpp @@ -23,4 +23,4 @@ void QuitAfterGameStop::executeTask() { APPLICATION->quit(); -} \ No newline at end of file +} From 424f4a72ffd4d5e6748559d2ce76ec2fd3b4cde1 Mon Sep 17 00:00:00 2001 From: dada513 Date: Sun, 27 Mar 2022 16:08:11 +0200 Subject: [PATCH 168/605] Inform user about possible issues when using a Portal as instance folder --- launcher/ui/pages/global/LauncherPage.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 6f7e1cc7..c3dde8e6 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2022 dada513 * * 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 +49,7 @@ #include "Application.h" #include "BuildConfig.h" #include "ui/themes/ITheme.h" +#include "Flatpak.h" #include #include @@ -141,6 +143,25 @@ void LauncherPage::on_instDirBrowseBtn_clicked() ui->instDirTextBox->setText(cooked_dir); } } + else if(Flatpak::IsFlatpak() && raw_dir.startsWith("/run/user")) + { + QMessageBox warning; + warning.setText(tr("You're trying to specify an instance folder " + "which was granted temporaily via Flatpak.\n" + "This is known to cause problems, " + "after a restart the launcher might break, " + "because it will no longer have access to that directory.\n\n" + "Granting PolyMC access to it via Flatseal is recommended.")); + warning.setInformativeText( + tr("Do you really want to use this path?\n" + "Selecting \"No\" will close this and not alter your instance path.")); + warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + int result = warning.exec(); + if (result == QMessageBox::Yes) + { + ui->instDirTextBox->setText(cooked_dir); + } + } else { ui->instDirTextBox->setText(cooked_dir); From 85f3fc9944914927e561ec2664922661c58264e2 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 26 Mar 2022 22:21:08 +0100 Subject: [PATCH 169/605] fix: remove "PolyMC" from strings --- launcher/launch/LaunchTask.cpp | 2 +- launcher/launch/steps/CheckJava.cpp | 4 ++-- launcher/minecraft/MinecraftInstance.cpp | 3 ++- launcher/ui/dialogs/AboutDialog.cpp | 7 +++++-- launcher/ui/pages/global/AccountListPage.cpp | 7 ++++--- launcher/ui/pages/instance/VersionPage.cpp | 2 +- launcher/ui/pages/modplatform/ModModel.cpp | 4 +++- 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index 231a6398..a38c97c8 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -212,7 +212,7 @@ shared_qobject_ptr LaunchTask::getLogModel() m_logModel->setMaxLines(m_instance->getConsoleMaxLines()); m_logModel->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); // FIXME: should this really be here? - m_logModel->setOverflowMessage(tr("PolyMC stopped watching the game log because the log length surpassed %1 lines.\n" + 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" " likely wasting harddrive space at an alarming rate!").arg(m_logModel->getMaxLines())); } diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp index d3f2148c..a31d0a25 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -87,14 +87,14 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) // Error message displayed if java can't start emit logLine(QString("Could not start java:"), MessageLevel::Error); emit logLines(result.errorLog.split('\n'), MessageLevel::Error); - emit logLine("\nCheck your PolyMC Java settings.", MessageLevel::Launcher); + emit logLine(QString("\nCheck your Java settings."), MessageLevel::Launcher); printSystemInfo(false, false); emitFailed(QString("Could not start java!")); return; } case JavaCheckResult::Validity::ReturnedInvalidData: { - emit logLine(QString("Java checker returned some invalid data PolyMC doesn't understand:"), MessageLevel::Error); + emit logLine(QString("Java checker returned some invalid data we don't understand:"), MessageLevel::Error); emit logLines(result.outLog.split('\n'), MessageLevel::Warning); emit logLine("\nMinecraft might not start properly.", MessageLevel::Launcher); printSystemInfo(false, false); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 90bb92a1..a0702a95 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1,4 +1,5 @@ #include "MinecraftInstance.h" +#include "BuildConfig.h" #include "minecraft/launch/CreateGameFolders.h" #include "minecraft/launch/ExtractNatives.h" #include "minecraft/launch/PrintInstanceInfo.h" @@ -435,7 +436,7 @@ QStringList MinecraftInstance::processMinecraftArgs( } // blatant self-promotion. - token_mapping["profile_name"] = token_mapping["version_name"] = "PolyMC"; + token_mapping["profile_name"] = token_mapping["version_name"] = BuildConfig.LAUNCHER_NAME; token_mapping["version_type"] = profile->getMinecraftVersionType(); diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index ef96cc23..d071da9a 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -14,6 +14,7 @@ */ #include "AboutDialog.h" +#include "BuildConfig.h" #include "ui_AboutDialog.h" #include #include "Application.h" @@ -33,13 +34,15 @@ QString getCreditsHtml() stream.setCodec(QTextCodec::codecForName("UTF-8")); stream << "
\n"; - stream << "

" << QObject::tr("PolyMC Developers", "About Credits") << "

\n"; + //: %1 is the name of the launcher, determined at build time, e.g. "PolyMC Developers" + stream << "

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

\n"; stream << "

swirl <swurl@swurl.xyz >

\n"; stream << "

LennyMcLennington <lenny@sneed.church>

\n"; stream << "
\n"; // TODO: possibly retrieve from git history at build time? - stream << "

" << QObject::tr("MultiMC Developers", "About Credits") << "

\n"; + //: %1 is the name of the launcher, determined at build time, e.g. "PolyMC Developers" + stream << "

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

\n"; stream << "

Andrew Okin <forkk@forkk.net>

\n"; stream << "

Petr Mrázek <peterix@gmail.com>

\n"; stream << "

Sky Welch <multimc@bunnies.io>

\n"; diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 1edba499..6e1e2183 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -161,10 +161,11 @@ void AccountListPage::on_actionAddMicrosoft_triggered() CustomMessageBox::selectable( this, tr("Microsoft Accounts not available"), + //: %1 refers to the launcher itself tr( - "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated PolyMC.\n\n" - "Please update both your operating system and PolyMC." - ), + "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated %1.\n\n" + "Please update both your operating system and %1." + ).arg(BuildConfig.LAUNCHER_NAME), QMessageBox::Warning )->exec(); return; diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 97c6fe8f..c417891b 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -422,7 +422,7 @@ void VersionPage::on_actionDownload_All_triggered() { CustomMessageBox::selectable( this, tr("Error"), - tr("PolyMC cannot download Minecraft or update instances unless you have at least " + tr("Cannot download Minecraft or update instances unless you have at least " "one account added.\nPlease add your Mojang or Minecraft account."), QMessageBox::Warning)->show(); return; diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index cc3c5326..b06eaf8e 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -1,5 +1,6 @@ #include "ModModel.h" +#include "BuildConfig.h" #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" @@ -186,7 +187,8 @@ void ListModel::searchRequestFailed(QString reason) if (jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { // 409 Gone, notify user to update QMessageBox::critical(nullptr, tr("Error"), - QString("%1 %2").arg(m_parent->displayName()).arg(tr("API version too old!\nPlease update PolyMC!"))); + //: %1 refers to the launcher itself + QString("%1 %2").arg(m_parent->displayName()).arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME))); // self-destruct (dynamic_cast((dynamic_cast(parent()))->parentWidget()))->reject(); } From 3a1feed7230abc7e55de86e4aa2a9470e3682f0e Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 26 Mar 2022 22:24:54 +0100 Subject: [PATCH 170/605] fix: update credits --- launcher/ui/dialogs/AboutDialog.cpp | 29 +++++++++++++++++++++++++++-- launcher/ui/dialogs/AboutDialog.ui | 9 +++------ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index d071da9a..81d1d7f7 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -21,10 +21,23 @@ #include "BuildConfig.h" #include +#include #include "HoeDown.h" 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() @@ -36,8 +49,20 @@ QString getCreditsHtml() //: %1 is the name of the launcher, determined at build time, e.g. "PolyMC Developers" stream << "

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

\n"; - stream << "

swirl <swurl@swurl.xyz >

\n"; - stream << "

LennyMcLennington <lenny@sneed.church>

\n"; + stream << QString("

LennyMcLennington %1

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

Sefa Eyeoglu (Scrumplex) %1

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

dada513 %1

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

txtsd %1

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

timoreo %1

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

Ezekiel Smith (ZekeSmith) %1

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

cozyGalvinism %1

\n") .arg(getGitHub("cozyGalvinism")); + stream << "
\n"; + + //: %1 is the name of the launcher, determined at build time, e.g. "PolyMC Contributors" + stream << "

" << QObject::tr("%1 Contributors", "About Credits").arg(BuildConfig.LAUNCHER_NAME) << "

\n"; + stream << QString("

DioEgizio %1

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

flowln %1

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

swirl %1

\n") .arg(getWebsite("https://swurl.xyz/")); stream << "
\n"; // TODO: possibly retrieve from git history at build time? diff --git a/launcher/ui/dialogs/AboutDialog.ui b/launcher/ui/dialogs/AboutDialog.ui index 58275c66..f9665c30 100644 --- a/launcher/ui/dialogs/AboutDialog.ui +++ b/launcher/ui/dialogs/AboutDialog.ui @@ -87,7 +87,7 @@ - + Qt::AlignCenter @@ -209,13 +209,10 @@ - - + + true - - Qt::TextBrowserInteraction - From 6054abaffb0af036a62b99d4b6990228b3cda3f1 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 26 Mar 2022 23:16:08 +0100 Subject: [PATCH 171/605] fix(credits): wrap UTF-8 text with QString --- launcher/ui/dialogs/AboutDialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index 81d1d7f7..5164551e 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -69,7 +69,7 @@ QString getCreditsHtml() //: %1 is the name of the launcher, determined at build time, e.g. "PolyMC Developers" stream << "

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

\n"; stream << "

Andrew Okin <forkk@forkk.net>

\n"; - stream << "

Petr Mrázek <peterix@gmail.com>

\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"; From ea60e48d9dbbabb4bc4418e691a4017d25ad4123 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 27 Mar 2022 13:13:32 +0200 Subject: [PATCH 172/605] chore: add license header chore: add license header --- launcher/launch/LaunchTask.cpp | 42 ++++++++++++++----- launcher/launch/steps/CheckJava.cpp | 40 +++++++++++++----- launcher/minecraft/MinecraftInstance.cpp | 35 ++++++++++++++++ launcher/ui/dialogs/AboutDialog.cpp | 40 +++++++++++++----- launcher/ui/pages/instance/VersionPage.cpp | 1 + .../modplatform/modrinth/ModrinthModel.cpp | 18 ++++++++ 6 files changed, 145 insertions(+), 31 deletions(-) diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index a38c97c8..d5442a2b 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -1,18 +1,38 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * Authors: Orochimarufan + * 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. * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . * - * 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. + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Authors: Orochimarufan + * + * 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 "launch/LaunchTask.h" diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp index a31d0a25..c2ebb334 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "CheckJava.h" diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index a0702a95..c7e60fda 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "MinecraftInstance.h" #include "BuildConfig.h" #include "minecraft/launch/CreateGameFolders.h" diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index 5164551e..8dadb755 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "AboutDialog.h" diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index c417891b..ed37dd1a 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index daa43e26..b788860a 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -1,3 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #include "ModrinthModel.h" #include "modplatform/modrinth/ModrinthPackIndex.h" From 659f93b1de381ecfcb1ac36957bc09e8a4921c92 Mon Sep 17 00:00:00 2001 From: Fayne Aldan Date: Sun, 27 Mar 2022 17:21:34 -0600 Subject: [PATCH 173/605] Fix POLYMC_JAVA_PATHS env not working on Windows --- launcher/java/JavaUtils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 6e5dfeae..c401093c 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -355,7 +355,7 @@ QList JavaUtils::FindJavaPaths() } } - return candidates; + return addJavasFromEnv(candidates); } #elif defined(Q_OS_MAC) From 3a7eeff135f92c807fdc066cb06d835f295b66d3 Mon Sep 17 00:00:00 2001 From: dada513 Date: Mon, 28 Mar 2022 20:55:03 +0200 Subject: [PATCH 174/605] Fix --- launcher/Application.cpp | 10 ++++++++ launcher/Application.h | 2 ++ launcher/CMakeLists.txt | 2 -- launcher/DesktopServices.cpp | 12 ++++----- launcher/Flatpak.cpp | 31 ----------------------- launcher/Flatpak.h | 21 --------------- launcher/ui/pages/global/LauncherPage.cpp | 18 ++++++------- 7 files changed, 26 insertions(+), 70 deletions(-) delete mode 100644 launcher/Flatpak.cpp delete mode 100644 launcher/Flatpak.h diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 33b1774c..abdfc06d 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1139,6 +1139,16 @@ std::vector Application::getValidApplicationThemes() return ret; } +bool Application::isFlatpak() +{ + #ifdef Q_OS_LINUX + QFileInfo check_file("/.flatpak-info"); + return check_file.exists(); + #else + return false; + #endif +} + void Application::setApplicationTheme(const QString& name, bool initial) { auto systemPalette = qApp->palette(); diff --git a/launcher/Application.h b/launcher/Application.h index c3e29ef5..54d9ba5f 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -104,6 +104,8 @@ public: QIcon getThemedIcon(const QString& name); + bool isFlatpak(); + void setIconTheme(const QString& name); std::vector getValidApplicationThemes(); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 1a16485e..98cb0a3b 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -582,8 +582,6 @@ SET(LAUNCHER_SOURCES ApplicationMessage.cpp # GUI - general utilities - Flatpak.h - Flatpak.cpp DesktopServices.h DesktopServices.cpp VersionProxyModel.h diff --git a/launcher/DesktopServices.cpp b/launcher/DesktopServices.cpp index f4226c15..c29cbe7d 100644 --- a/launcher/DesktopServices.cpp +++ b/launcher/DesktopServices.cpp @@ -37,7 +37,7 @@ #include #include #include -#include "Flatpak.h" +#include "Application.h" /** * This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing. @@ -119,7 +119,7 @@ bool openDirectory(const QString &path, bool ensureExists) return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath())); }; #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - if(!Flatpak::IsFlatpak()) + if(!APPLICATION->isFlatpak()) { return IndirectOpen(f); } @@ -140,7 +140,7 @@ bool openFile(const QString &path) return QDesktopServices::openUrl(QUrl::fromLocalFile(path)); }; #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - if(!Flatpak::IsFlatpak()) + if(!APPLICATION->isFlatpak()) { return IndirectOpen(f); } @@ -158,7 +158,7 @@ bool openFile(const QString &application, const QString &path, const QString &wo qDebug() << "Opening file" << path << "using" << application; #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) // FIXME: the pid here is fake. So if something depends on it, it will likely misbehave - if(!Flatpak::IsFlatpak()) + if(!APPLICATION->isFlatpak()) { return IndirectOpen([&]() { @@ -178,7 +178,7 @@ bool run(const QString &application, const QStringList &args, const QString &wor { qDebug() << "Running" << application << "with args" << args.join(' '); #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - if(!Flatpak::IsFlatpak()) + if(!APPLICATION->isFlatpak()) { // FIXME: the pid here is fake. So if something depends on it, it will likely misbehave return IndirectOpen([&]() @@ -203,7 +203,7 @@ bool openUrl(const QUrl &url) return QDesktopServices::openUrl(url); }; #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - if(!Flatpak::IsFlatpak()) + if(!APPLICATION->isFlatpak()) { return IndirectOpen(f); } diff --git a/launcher/Flatpak.cpp b/launcher/Flatpak.cpp deleted file mode 100644 index 2c28903c..00000000 --- a/launcher/Flatpak.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - Minecraft Launcher - * Copyright (C) 2022 dada513 - * - * 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 - -namespace Flatpak -{ - bool IsFlatpak() - { - #ifdef Q_OS_LINUX - QFileInfo check_file("/.flatpak-info"); - return check_file.exists(); - #else - return false; - #endif - } -} diff --git a/launcher/Flatpak.h b/launcher/Flatpak.h deleted file mode 100644 index 8fd315fb..00000000 --- a/launcher/Flatpak.h +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - Minecraft Launcher - * Copyright (C) 2022 dada513 - * - * 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 . - */ -namespace Flatpak -{ - bool IsFlatpak(); -} diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index c3dde8e6..42ad5ae3 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -49,7 +49,6 @@ #include "Application.h" #include "BuildConfig.h" #include "ui/themes/ITheme.h" -#include "Flatpak.h" #include #include @@ -136,28 +135,27 @@ void LauncherPage::on_instDirBrowseBtn_clicked() warning.setInformativeText( tr("Do you really want to use this path? " "Selecting \"No\" will close this and not alter your instance path.")); - warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + warning.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); int result = warning.exec(); - if (result == QMessageBox::Yes) + if (result == QMessageBox::Ok) { ui->instDirTextBox->setText(cooked_dir); } } - else if(Flatpak::IsFlatpak() && raw_dir.startsWith("/run/user")) + else if(APPLICATION->isFlatpak() && raw_dir.startsWith("/run/user")) { QMessageBox warning; warning.setText(tr("You're trying to specify an instance folder " "which was granted temporaily via Flatpak.\n" - "This is known to cause problems, " - "after a restart the launcher might break, " + "This is known to cause problems. " + "After a restart the launcher might break, " "because it will no longer have access to that directory.\n\n" "Granting PolyMC access to it via Flatseal is recommended.")); warning.setInformativeText( - tr("Do you really want to use this path?\n" - "Selecting \"No\" will close this and not alter your instance path.")); - warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + tr("Do you want to proceed anyway?")); + warning.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); int result = warning.exec(); - if (result == QMessageBox::Yes) + if (result == QMessageBox::Ok) { ui->instDirTextBox->setText(cooked_dir); } From 954074942e6361223b0beee6eff16a73d2a936a7 Mon Sep 17 00:00:00 2001 From: dada513 Date: Mon, 28 Mar 2022 20:56:24 +0200 Subject: [PATCH 175/605] =?UTF-8?q?=F0=9F=98=A2=20fix=20bully?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dc4cd1c5..c9a762f5 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,4 @@ result/ # Flatpak .flatpak-builder -flatbuild \ No newline at end of file +flatbuild From 4d8bf0b6212fffa498cc79a283166fbe06e90669 Mon Sep 17 00:00:00 2001 From: Fayne Aldan Date: Mon, 28 Mar 2022 15:55:54 -0600 Subject: [PATCH 176/605] Convert \s in Windows POLYMC_JAVA_PATHS Allows you to use either `\` or `/` on Windows --- launcher/java/JavaUtils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 6e5dfeae..1891fa84 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -153,7 +153,7 @@ QStringList addJavasFromEnv(QList javas) { QByteArray env = qgetenv("POLYMC_JAVA_PATHS"); #if defined(Q_OS_WIN32) - QList javaPaths = QString::fromLocal8Bit(env).split(QLatin1String(";")); + QList javaPaths = QString::fromLocal8Bit(env).replace("\\", "/").split(QLatin1String(";")); #else QList javaPaths = QString::fromLocal8Bit(env).split(QLatin1String(":")); #endif From 9202996c623fca772a0d1f63ba82e57166428fc2 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 29 Mar 2022 15:25:21 +0200 Subject: [PATCH 177/605] fix(i18n): remove brand names from translations --- launcher/ui/dialogs/AboutDialog.ui | 2 +- launcher/ui/dialogs/UpdateDialog.ui | 2 +- launcher/ui/pages/global/MinecraftPage.ui | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/launcher/ui/dialogs/AboutDialog.ui b/launcher/ui/dialogs/AboutDialog.ui index f9665c30..70c5009d 100644 --- a/launcher/ui/dialogs/AboutDialog.ui +++ b/launcher/ui/dialogs/AboutDialog.ui @@ -80,7 +80,7 @@
- PolyMC + Launcher Qt::AlignCenter diff --git a/launcher/ui/dialogs/UpdateDialog.ui b/launcher/ui/dialogs/UpdateDialog.ui index bd94a554..5eb9d88a 100644 --- a/launcher/ui/dialogs/UpdateDialog.ui +++ b/launcher/ui/dialogs/UpdateDialog.ui @@ -11,7 +11,7 @@ - PolyMC Update + Launcher Update diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index decc9b8b..c18ab34b 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -173,20 +173,20 @@ - <html><head/><body><p>PolyMC will automatically reopen when the game crashes or exits.</p></body></html> + <html><head/><body><p>The launcher will automatically reopen when the game crashes or exits.</p></body></html> - Close PolyMC after game window opens + Close the launcher after game window opens - <html><head/><body><p>PolyMC will automatically exit if the game crashes or exists.</p></body></html> + <html><head/><body><p>The launcher will automatically quit after the game exits or crashes.</p></body></html> - Quit PolyMC after game window stops + Quit the launcher after game window closes From 92e5e0e95b6932feae0ccfdbcfc1183985e5021e Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Wed, 30 Mar 2022 10:51:37 -0400 Subject: [PATCH 178/605] Make launcher icon grayscale for pe_light theme --- .../resources/pe_light/scalable/launcher.svg | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/launcher/resources/pe_light/scalable/launcher.svg b/launcher/resources/pe_light/scalable/launcher.svg index c192d503..a9dfe87a 100644 --- a/launcher/resources/pe_light/scalable/launcher.svg +++ b/launcher/resources/pe_light/scalable/launcher.svg @@ -3,18 +3,18 @@ - - - + + + - - - - - - + + + + + + From 59b3e30821bd20a3dc9e9fe4617ffa461f3a89b8 Mon Sep 17 00:00:00 2001 From: dada513 Date: Thu, 31 Mar 2022 16:11:04 +0200 Subject: [PATCH 179/605] Scrumplex moment --- launcher/Application.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index af1cfe85..91b7b82c 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1143,8 +1143,7 @@ std::vector Application::getValidApplicationThemes() bool Application::isFlatpak() { #ifdef Q_OS_LINUX - QFileInfo check_file("/.flatpak-info"); - return check_file.exists(); + return QFile::exists("/.flatpak-info"); #else return false; #endif From 64ca96f470efda6ee643e52e80105eff2790c22b Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 31 Mar 2022 18:45:17 +0200 Subject: [PATCH 180/605] feat: track and display world size --- launcher/minecraft/World.cpp | 22 ++++++++++++++++++++++ launcher/minecraft/World.h | 6 ++++++ launcher/minecraft/WorldList.cpp | 18 +++++++++++++++++- launcher/minecraft/WorldList.h | 4 +++- 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index 2937c116..e45f042d 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "World.h" #include "GZip.h" @@ -187,6 +188,25 @@ 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.absolutePath(), QDirIterator::Subdirectories); + int64_t total = 0; + while (it.hasNext()) { + total += it.fileInfo().size(); + it.next(); + } + return total; + } + return -1; +} + World::World(const QFileInfo &file) { repath(file); @@ -196,6 +216,7 @@ void World::repath(const QFileInfo &file) { m_containerFile = file; m_folderName = file.fileName(); + m_size = calculateWorldSize(file); if(file.isFile() && file.suffix() == "zip") { m_iconFile = QString(); @@ -482,6 +503,7 @@ void World::loadFromLevelDat(QByteArray data) if(randomSeed) { qDebug() << "Seed:" << *randomSeed; } + qDebug() << "Size:" << m_size; qDebug() << "GameType:" << m_gameType.toLogString(); } diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h index 35e32788..62af744a 100644 --- a/launcher/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -17,6 +17,7 @@ #include #include #include +#include struct GameType { GameType() = default; @@ -52,6 +53,10 @@ public: { return m_iconFile; } + int64_t bytes() const + { + return m_size; + } QDateTime lastPlayed() const { return m_lastPlayed; @@ -105,6 +110,7 @@ protected: QString m_iconFile; QDateTime levelDatTime; QDateTime m_lastPlayed; + int64_t m_size; int64_t m_randomSeed = 0; GameType m_gameType; bool is_valid = false; diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index dcdbc321..344bea63 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -14,6 +14,8 @@ */ #include "WorldList.h" + +#include "Application.h" #include #include #include @@ -150,7 +152,7 @@ bool WorldList::resetIcon(int row) int WorldList::columnCount(const QModelIndex &parent) const { - return 3; + return 4; } QVariant WorldList::data(const QModelIndex &index, int role) const @@ -164,6 +166,8 @@ QVariant WorldList::data(const QModelIndex &index, int role) const if (row < 0 || row >= worlds.size()) return QVariant(); + QLocale locale; + auto & world = worlds[row]; switch (role) { @@ -179,6 +183,9 @@ QVariant WorldList::data(const QModelIndex &index, int role) const case LastPlayedColumn: return world.lastPlayed(); + case SizeColumn: + return locale.formattedDataSize(world.bytes()); + default: return QVariant(); } @@ -207,6 +214,10 @@ QVariant WorldList::data(const QModelIndex &index, int role) const { return world.lastPlayed(); } + case SizeRole: + { + return locale.formattedDataSize(world.bytes()); + } case IconFileRole: { return world.iconFile(); @@ -229,6 +240,9 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol return tr("Game Mode"); case LastPlayedColumn: return tr("Last Played"); + case SizeColumn: + //: World size on disk + return tr("Size"); default: return QVariant(); } @@ -242,6 +256,8 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol return tr("Game mode of the world."); case LastPlayedColumn: return tr("Date and time the world was last played."); + case SizeColumn: + return tr("Size of the world on disk."); default: return QVariant(); } diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h index 8e238ee3..5138e583 100644 --- a/launcher/minecraft/WorldList.h +++ b/launcher/minecraft/WorldList.h @@ -32,7 +32,8 @@ public: { NameColumn, GameModeColumn, - LastPlayedColumn + LastPlayedColumn, + SizeColumn }; enum Roles @@ -43,6 +44,7 @@ public: NameRole, GameModeRole, LastPlayedRole, + SizeRole, IconFileRole }; From 48a6380e31a29be3aad0e87b3c2938a7ca18bb4a Mon Sep 17 00:00:00 2001 From: dada513 Date: Thu, 31 Mar 2022 20:39:10 +0200 Subject: [PATCH 181/605] Fix modrinth usable URL in mod downloader --- launcher/modplatform/modrinth/ModrinthPackIndex.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index a4e56d4f..5b75f034 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -12,7 +12,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireString(obj, "project_id"); pack.name = Json::requireString(obj, "title"); - pack.websiteUrl = Json::ensureString(obj, "page_url", ""); + pack.websiteUrl = "https://modrinth.com/mod/" + Json::ensureString(obj, "slug", ""); pack.description = Json::ensureString(obj, "description", ""); pack.logoUrl = Json::requireString(obj, "icon_url"); From c389a711ed31c11f60f5462e46fd3bcf80359c60 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 1 Apr 2022 13:14:04 +0200 Subject: [PATCH 182/605] fix: remove redundant include --- launcher/minecraft/World.h | 1 - 1 file changed, 1 deletion(-) diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h index 62af744a..0f587620 100644 --- a/launcher/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -17,7 +17,6 @@ #include #include #include -#include struct GameType { GameType() = default; From 9b8493c30499e06bbef7b96ff415f80c140c1a7f Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 1 Apr 2022 09:10:51 -0300 Subject: [PATCH 183/605] feat: Use a single progress dialog when doing multiple tasks This puts all mod downloading tasks inside a SequentialTask, which is, for more than one task, a multi step task. This is handled by the ProgressDialog by showing both the global progress of tasks executed, and the individual progress of each of them. --- launcher/tasks/SequentialTask.cpp | 50 ++++++++++---- launcher/tasks/SequentialTask.h | 28 ++++++-- launcher/tasks/Task.h | 68 +++++++++----------- launcher/ui/dialogs/ProgressDialog.cpp | 22 ++++++- launcher/ui/dialogs/ProgressDialog.h | 5 +- launcher/ui/dialogs/ProgressDialog.ui | 67 ++++++++++--------- launcher/ui/pages/instance/ModFolderPage.cpp | 37 +++++------ 7 files changed, 172 insertions(+), 105 deletions(-) diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp index a66b9d78..1573e476 100644 --- a/launcher/tasks/SequentialTask.cpp +++ b/launcher/tasks/SequentialTask.cpp @@ -1,7 +1,23 @@ #include "SequentialTask.h" -SequentialTask::SequentialTask(QObject *parent) : Task(parent), m_currentIndex(-1) +SequentialTask::SequentialTask(QObject* parent, const QString& task_name) : Task(parent), m_name(task_name), m_currentIndex(-1) {} + +SequentialTask::~SequentialTask() { + for(auto task : m_queue){ + if(task) + task->deleteLater(); + } +} + +auto SequentialTask::getStepProgress() const -> qint64 +{ + return m_stepProgress; +} + +auto SequentialTask::getStepTotalProgress() const -> qint64 +{ + return m_stepTotalProgress; } void SequentialTask::addTask(Task::Ptr task) @@ -15,16 +31,24 @@ void SequentialTask::executeTask() startNext(); } +bool SequentialTask::abort() +{ + bool succeeded = true; + for (auto& task : m_queue) { + if (!task->abort()) succeeded = false; + } + + return succeeded; +} + void SequentialTask::startNext() { - if (m_currentIndex != -1) - { + if (m_currentIndex != -1) { Task::Ptr previous = m_queue[m_currentIndex]; disconnect(previous.get(), 0, this, 0); } m_currentIndex++; - if (m_queue.isEmpty() || m_currentIndex >= m_queue.size()) - { + if (m_queue.isEmpty() || m_currentIndex >= m_queue.size()) { emitSucceeded(); return; } @@ -33,23 +57,27 @@ void SequentialTask::startNext() connect(next.get(), SIGNAL(status(QString)), this, SLOT(subTaskStatus(QString))); connect(next.get(), SIGNAL(progress(qint64, qint64)), this, SLOT(subTaskProgress(qint64, qint64))); connect(next.get(), SIGNAL(succeeded()), this, SLOT(startNext())); + + setStatus(tr("Executing task %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size())); next->start(); } -void SequentialTask::subTaskFailed(const QString &msg) +void SequentialTask::subTaskFailed(const QString& msg) { emitFailed(msg); } -void SequentialTask::subTaskStatus(const QString &msg) +void SequentialTask::subTaskStatus(const QString& msg) { - setStatus(msg); + setStepStatus(m_queue[m_currentIndex]->getStatus()); } void SequentialTask::subTaskProgress(qint64 current, qint64 total) { - if(total == 0) - { + if (total == 0) { setProgress(0, 100); return; } - setProgress(current, total); + setProgress(m_currentIndex, m_queue.count()); + + m_stepProgress = current; + m_stepTotalProgress = total; } diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h index 027744f3..5b3c0111 100644 --- a/launcher/tasks/SequentialTask.h +++ b/launcher/tasks/SequentialTask.h @@ -9,13 +9,21 @@ class SequentialTask : public Task { Q_OBJECT public: - explicit SequentialTask(QObject *parent = 0); - virtual ~SequentialTask() {}; + explicit SequentialTask(QObject *parent = nullptr, const QString& task_name = ""); + virtual ~SequentialTask(); + + inline auto isMultiStep() const -> bool override { return m_queue.size() > 1; }; + auto getStepProgress() const -> qint64 override; + auto getStepTotalProgress() const -> qint64 override; + + inline auto getStepStatus() const -> QString override { return m_step_status; } void addTask(Task::Ptr task); -protected: - void executeTask(); +protected slots: + void executeTask() override; +public slots: + bool abort() override; private slots: @@ -24,7 +32,19 @@ slots: void subTaskStatus(const QString &msg); void subTaskProgress(qint64 current, qint64 total); +signals: + void stepStatus(QString status); + private: + void setStepStatus(QString status) { m_step_status = status; }; + +private: + QString m_name; + QString m_step_status; + QQueue m_queue; int m_currentIndex; + + qint64 m_stepProgress = 0; + qint64 m_stepTotalProgress = 100; }; diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 9cf08dbd..47c249b3 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -21,29 +21,27 @@ #include "QObjectPtr.h" -class Task : public QObject -{ +class Task : public QObject { Q_OBJECT -public: + public: using Ptr = shared_qobject_ptr; - enum class State - { - Inactive, - Running, - Succeeded, - Failed, - AbortedByUser - }; + enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser }; -public: - explicit Task(QObject *parent = 0); - virtual ~Task() {}; + public: + explicit Task(QObject* parent = 0); + virtual ~Task() = default; bool isRunning() const; bool isFinished() const; bool wasSuccessful() const; + /*! + * MultiStep tasks are combinations of multiple tasks into a single logical task. + * The main usage of this is in SequencialTask. + */ + virtual auto isMultiStep() const -> bool { return false; } + /*! * Returns the string that was passed to emitFailed as the error message when the task failed. * If the task hasn't failed, returns an empty string. @@ -54,52 +52,45 @@ public: virtual bool canAbort() const { return false; } - QString getStatus() - { - return m_status; - } + QString getStatus() { return m_status; } + virtual auto getStepStatus() const -> QString { return {}; } - qint64 getProgress() - { - return m_progress; - } + qint64 getProgress() { return m_progress; } + qint64 getTotalProgress() { return m_progressTotal; } + virtual auto getStepProgress() const -> qint64 { return 0; } + virtual auto getStepTotalProgress() const -> qint64 { return 100; } - qint64 getTotalProgress() - { - return m_progressTotal; - } + protected: + void logWarning(const QString& line); -protected: - void logWarning(const QString & line); - -private: + private: QString describe(); -signals: + signals: void started(); - void progress(qint64 current, qint64 total); + virtual void progress(qint64 current, qint64 total); void finished(); void succeeded(); void failed(QString reason); void status(QString status); -public slots: + public slots: virtual void start(); virtual bool abort() { return false; }; -protected: + protected: virtual void executeTask() = 0; -protected slots: + protected slots: virtual void emitSucceeded(); virtual void emitAborted(); virtual void emitFailed(QString reason); -public slots: - void setStatus(const QString &status); + public slots: + void setStatus(const QString& status); void setProgress(qint64 current, qint64 total); -private: + private: State m_state = State::Inactive; QStringList m_Warnings; QString m_failReason = ""; @@ -107,4 +98,3 @@ private: int m_progress = 0; int m_progressTotal = 100; }; - diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index 4b092859..648bd88b 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -81,6 +81,12 @@ int ProgressDialog::execWithTask(Task *task) connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString &))); connect(task, SIGNAL(progress(qint64, qint64)), SLOT(changeProgress(qint64, qint64))); + m_is_multi_step = task->isMultiStep(); + if(!m_is_multi_step){ + ui->globalStatusLabel->setHidden(true); + ui->globalProgressBar->setHidden(true); + } + // if this didn't connect to an already running task, invoke start if(!task->isRunning()) { @@ -152,14 +158,24 @@ void ProgressDialog::onTaskSucceeded() void ProgressDialog::changeStatus(const QString &status) { - ui->statusLabel->setText(status); + ui->statusLabel->setText(task->getStepStatus()); + ui->globalStatusLabel->setText(status); updateSize(); } void ProgressDialog::changeProgress(qint64 current, qint64 total) { - ui->taskProgressBar->setMaximum(total); - ui->taskProgressBar->setValue(current); + ui->globalProgressBar->setMaximum(total); + ui->globalProgressBar->setValue(current); + + if(!m_is_multi_step){ + ui->taskProgressBar->setMaximum(total); + ui->taskProgressBar->setValue(current); + } + else{ + ui->taskProgressBar->setMaximum(task->getStepProgress()); + ui->taskProgressBar->setValue(task->getStepTotalProgress()); + } } void ProgressDialog::keyPressEvent(QKeyEvent *e) diff --git a/launcher/ui/dialogs/ProgressDialog.h b/launcher/ui/dialogs/ProgressDialog.h index b28ad4fa..0b4b78a4 100644 --- a/launcher/ui/dialogs/ProgressDialog.h +++ b/launcher/ui/dialogs/ProgressDialog.h @@ -19,6 +19,7 @@ #include class Task; +class SequentialTask; namespace Ui { @@ -35,7 +36,7 @@ public: void updateSize(); - int execWithTask(Task *task); + int execWithTask(Task* task); int execWithTask(std::unique_ptr &&task); int execWithTask(std::unique_ptr &task); @@ -68,4 +69,6 @@ private: Ui::ProgressDialog *ui; Task *task; + + bool m_is_multi_step = false; }; diff --git a/launcher/ui/dialogs/ProgressDialog.ui b/launcher/ui/dialogs/ProgressDialog.ui index 04b8fef3..bf119a78 100644 --- a/launcher/ui/dialogs/ProgressDialog.ui +++ b/launcher/ui/dialogs/ProgressDialog.ui @@ -2,14 +2,6 @@ ProgressDialog - - - 0 - 0 - 400 - 100 - - 400 @@ -26,27 +18,7 @@ Please wait... - - - - Task Status... - - - true - - - - - - - 24 - - - false - - - - + @@ -59,6 +31,43 @@ + + + + Global Task Status... + + + + + + + Task Status... + + + true + + + + + + + 24 + + + false + + + + + + + true + + + 24 + + + diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 599f0e11..fcb6022d 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -58,6 +58,7 @@ #include "Version.h" #include "ui/dialogs/ProgressDialog.h" +#include "tasks/SequentialTask.h" namespace { // FIXME: wasteful @@ -394,25 +395,25 @@ void ModFolderPage::on_actionInstall_mods_triggered() return; } ModDownloadDialog mdownload(m_mods, this, m_inst); - if(mdownload.exec()) { - for(auto task : mdownload.getTasks()){ - connect(task, &Task::failed, [this, task](QString reason) { - task->deleteLater(); - CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); - }); - connect(task, &Task::succeeded, [this, task]() { - QStringList warnings = task->warnings(); - if (warnings.count()) { - CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), - QMessageBox::Warning)->show(); - } - task->deleteLater(); - }); - ProgressDialog loadDialog(this); - loadDialog.setSkipButton(true, tr("Abort")); - loadDialog.execWithTask(task); - m_mods->update(); + if (mdownload.exec()) { + SequentialTask* tasks = new SequentialTask(this); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->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_mods->update(); } } From e8697068fb5baa454ad97ae272726f98d2108f94 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 1 Apr 2022 15:00:05 +0200 Subject: [PATCH 184/605] fix: codestyle --- launcher/minecraft/World.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index e45f042d..9cf67ed9 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -198,7 +198,8 @@ int64_t calculateWorldSize(const QFileInfo &file) { QDirIterator it(file.absolutePath(), QDirIterator::Subdirectories); int64_t total = 0; - while (it.hasNext()) { + while (it.hasNext()) + { total += it.fileInfo().size(); it.next(); } From 2c07f758a0c1e4d6ee7a9e6df1faf6b9aa3824a1 Mon Sep 17 00:00:00 2001 From: Ilan Joselevich Date: Sun, 13 Mar 2022 18:18:46 +0200 Subject: [PATCH 185/605] nix: refactor flake (drop flake-utils) --- flake.lock | 24 ++--------- flake.nix | 74 +++++++++++++-------------------- packages/nix/polymc/default.nix | 15 +++---- 3 files changed, 41 insertions(+), 72 deletions(-) diff --git a/flake.lock b/flake.lock index f2205416..c849bca1 100644 --- a/flake.lock +++ b/flake.lock @@ -16,21 +16,6 @@ "type": "github" } }, - "flake-utils": { - "locked": { - "lastModified": 1642700792, - "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "libnbtplusplus": { "flake": false, "locked": { @@ -49,16 +34,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1643169865, - "narHash": "sha256-+KIpNRazbc8Gac9jdWCKQkFv9bjceaLaLhlwqUEYu8c=", + "lastModified": 1646955661, + "narHash": "sha256-AYLta1PubJnrkv15+7G+6ErW5m9NcI9wSdJ+n7pKAe0=", "owner": "nixos", "repo": "nixpkgs", - "rev": "945ec499041db73043f745fad3b2a3a01e826081", + "rev": "e9545762b032559c27d8ec9141ed63ceca1aa1ac", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixos-unstable", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } @@ -82,7 +67,6 @@ "root": { "inputs": { "flake-compat": "flake-compat", - "flake-utils": "flake-utils", "libnbtplusplus": "libnbtplusplus", "nixpkgs": "nixpkgs", "quazip": "quazip" diff --git a/flake.nix b/flake.nix index 5f95b4e6..e59d6be8 100644 --- a/flake.nix +++ b/flake.nix @@ -1,50 +1,34 @@ { - description = "PolyMC flake"; - inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; - inputs.flake-utils.url = "github:numtide/flake-utils"; - inputs.flake-compat = { - url = "github:edolstra/flake-compat"; - flake = false; - }; - inputs.libnbtplusplus = { - url = "github:multimc/libnbtplusplus"; - flake = false; - }; - inputs.quazip = { - url = "github:stachenov/quazip"; - flake = false; + description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of MultiMC)"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; + libnbtplusplus = { url = "github:multimc/libnbtplusplus"; flake = false; }; + quazip = { url = "github:stachenov/quazip"; flake = false; }; }; - outputs = args@{ self, nixpkgs, flake-utils, libnbtplusplus, quazip, ... }: + outputs = { self, nixpkgs, libnbtplusplus, quazip, ... }: let - systems = [ - "aarch64-linux" - # "aarch64-darwin" # qtbase is currently broken - "i686-linux" - "x86_64-darwin" - "x86_64-linux" - ]; - in { - overlay = final: prev: { - inherit (self.packages.${final.system}) polymc; - }; - } // flake-utils.lib.eachSystem systems (system: - let pkgs = import nixpkgs { inherit system; }; - in { - packages = { - polymc = pkgs.libsForQt5.callPackage ./packages/nix/polymc { - inherit self; - submoduleQuazip = quazip; - submoduleNbt = libnbtplusplus; - }; - }; - apps = { - polymc = flake-utils.lib.mkApp { - name = "polymc"; - drv = self.packages.${system}.polymc; - }; - }; - defaultPackage = self.packages.${system}.polymc; - defaultApp = self.apps.${system}.polymc; - }); + # Generate a user-friendly version number. + version = builtins.substring 0 8 self.lastModifiedDate; + + # System types to support (qtbase is currently broken for "aarch64-darwin") + supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" ]; + + # Helper function to generate an attrset '{ x86_64-linux = f "x86_64-linux"; ... }'. + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + # Nixpkgs instantiated for supported system types. + pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system}); + in + { + packages = forAllSystems (system: { polymc = pkgs.${system}.libsForQt5.callPackage ./packages/nix/polymc { inherit version self quazip libnbtplusplus; }; }); + defaultPackage = forAllSystems (system: self.packages.${system}.polymc); + + apps = forAllSystems (system: { polymc = { type = "app"; program = "${self.defaultPackage.${system}}/bin/polymc"; }; }); + defaultApp = forAllSystems (system: self.apps.${system}.polymc); + + overlay = final: prev: { polymc = self.defaultPackage.${final.system}; }; + }; } diff --git a/packages/nix/polymc/default.nix b/packages/nix/polymc/default.nix index 63fc6b7e..bea62b26 100644 --- a/packages/nix/polymc/default.nix +++ b/packages/nix/polymc/default.nix @@ -14,10 +14,11 @@ , libGL , msaClientID ? "" -# flake + # flake , self -, submoduleNbt -, submoduleQuazip +, version +, libnbtplusplus +, quazip }: let @@ -30,7 +31,7 @@ let libXxf86vm libpulseaudio libGL - ]; + ]; # This variable will be passed to Minecraft by PolyMC gameLibraryPath = libpath + ":/run/opengl-driver/lib"; @@ -38,7 +39,7 @@ in mkDerivation rec { pname = "polymc"; - version = "nightly"; + inherit version; src = lib.cleanSource self; @@ -57,8 +58,8 @@ mkDerivation rec { # Copy submodules inputs rm -rf source/libraries/{libnbtplusplus,quazip} mkdir source/libraries/{libnbtplusplus,quazip} - cp -a ${submoduleNbt}/* source/libraries/libnbtplusplus - cp -a ${submoduleQuazip}/* source/libraries/quazip + cp -a ${libnbtplusplus}/* source/libraries/libnbtplusplus + cp -a ${quazip}/* source/libraries/quazip chmod a+r+w source/libraries/{libnbtplusplus,quazip}/* ''; From 6e6d495bc7d937d46b43f368b315f54f658e05f5 Mon Sep 17 00:00:00 2001 From: Ilan Joselevich Date: Sun, 13 Mar 2022 23:41:45 +0200 Subject: [PATCH 186/605] nix: build with latest jdk version --- packages/nix/polymc/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nix/polymc/default.nix b/packages/nix/polymc/default.nix index bea62b26..e352209a 100644 --- a/packages/nix/polymc/default.nix +++ b/packages/nix/polymc/default.nix @@ -44,7 +44,7 @@ mkDerivation rec { src = lib.cleanSource self; nativeBuildInputs = [ cmake ninja file makeWrapper ]; - buildInputs = [ qtbase jdk8 zlib ]; + buildInputs = [ qtbase jdk zlib ]; dontWrapQtApps = true; From 131a04653f0f269b640207d68c9c9d578cef2881 Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Sat, 2 Apr 2022 07:52:07 +1000 Subject: [PATCH 187/605] Update License --- program_info/LICENSE | 57 +++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/program_info/LICENSE b/program_info/LICENSE index 40cc6059..c68207dc 100644 --- a/program_info/LICENSE +++ b/program_info/LICENSE @@ -1,4 +1,4 @@ -Attribution-NonCommercial-ShareAlike 4.0 International +Attribution-ShareAlike 4.0 International This license only applies to the logos and branding in this folder. @@ -56,18 +56,18 @@ exhaustive, and do not form part of our licenses. ======================================================================= -Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International -Public License +Creative Commons Attribution-ShareAlike 4.0 International Public +License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons -Attribution-NonCommercial-ShareAlike 4.0 International Public License -("Public License"). To the extent this Public License may be -interpreted as a contract, You are granted the Licensed Rights in -consideration of Your acceptance of these terms and conditions, and the -Licensor grants You such rights in consideration of benefits the -Licensor receives from making the Licensed Material available under -these terms and conditions. +Attribution-ShareAlike 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. Section 1 -- Definitions. @@ -86,7 +86,7 @@ Section 1 -- Definitions. and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. - c. BY-NC-SA Compatible License means a license listed at + c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License. @@ -110,7 +110,7 @@ Section 1 -- Definitions. g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this - Public License are Attribution, NonCommercial, and ShareAlike. + Public License are Attribution and ShareAlike. h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public @@ -124,15 +124,7 @@ Section 1 -- Definitions. j. Licensor means the individual(s) or entity(ies) granting rights under this Public License. - k. NonCommercial means not primarily intended for or directed towards - commercial advantage or monetary compensation. For purposes of - this Public License, the exchange of the Licensed Material for - other material subject to Copyright and Similar Rights by digital - file-sharing or similar means is NonCommercial provided there is - no payment of monetary compensation in connection with the - exchange. - - l. Share means to provide material to the public by any means or + k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material @@ -140,13 +132,13 @@ Section 1 -- Definitions. public may access the material from a place and at a time individually chosen by them. - m. Sui Generis Database Rights means rights other than copyright + l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. - n. You means the individual or entity exercising the Licensed Rights + m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. @@ -160,10 +152,9 @@ Section 2 -- Scope. exercise the Licensed Rights in the Licensed Material to: a. reproduce and Share the Licensed Material, in whole or - in part, for NonCommercial purposes only; and + in part; and - b. produce, reproduce, and Share Adapted Material for - NonCommercial purposes only. + b. produce, reproduce, and Share Adapted Material. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public @@ -231,9 +222,7 @@ Section 2 -- Scope. Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly - reserves any right to collect such royalties, including when - the Licensed Material is used other than for NonCommercial - purposes. + reserves any right to collect such royalties. Section 3 -- License Conditions. @@ -278,6 +267,7 @@ following conditions. reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. + 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. @@ -289,7 +279,7 @@ following conditions. 1. The Adapter's License You apply must be a Creative Commons license with the same License Elements, this version or - later, or a BY-NC-SA Compatible License. + later, or a BY-SA Compatible License. 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition @@ -309,15 +299,14 @@ apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial - portion of the contents of the database for NonCommercial purposes - only; + portion of the contents of the database; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, - including for purposes of Section 3(b); and + including for purposes of Section 3(b); and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. @@ -417,6 +406,7 @@ Section 8 -- Interpretation. that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. + ======================================================================= Creative Commons is not a party to its public @@ -437,3 +427,4 @@ the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. + From 9180c751d8d6b51e1fc34ce84b2a705954b98d31 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 2 Apr 2022 00:54:48 +0200 Subject: [PATCH 188/605] fix(launch/VerifyJava): reword log output --- launcher/minecraft/launch/VerifyJavaInstall.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp index 42754d44..99809f82 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.cpp +++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -64,12 +64,12 @@ void VerifyJavaInstall::executeTask() { return; } - emit logLine(tr("Instance not compatible with Java major version %1.\n" - "Switch the Java version of this instance to one of the following:").arg(javaVersion.major()), + emit logLine(tr("This instance is not compatible with Java version %1.\n" + "Please switch to one of the following Java versions for this instance:").arg(javaVersion.major()), MessageLevel::Error); - for (auto major: compatibleMajors) + for (auto major : compatibleMajors) { - emit logLine(tr("Java %1").arg(major), MessageLevel::Error); + emit logLine(tr("Java version %1").arg(major), MessageLevel::Error); } emitFailed(QString("Incompatible Java major version")); } From c098be40ab8076446f1d3f815e3a9df7c3be53a1 Mon Sep 17 00:00:00 2001 From: Ilan Joselevich Date: Sat, 2 Apr 2022 02:35:44 +0300 Subject: [PATCH 189/605] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'flake-compat': 'github:edolstra/flake-compat/b7547d3eed6f32d06102ead8991ec52ab0a4f1a7' (2022-01-03) → 'github:edolstra/flake-compat/64a525ee38886ab9028e6f61790de0832aa3ef03' (2022-03-25) • Updated input 'nixpkgs': 'github:nixos/nixpkgs/e9545762b032559c27d8ec9141ed63ceca1aa1ac' (2022-03-10) → 'github:nixos/nixpkgs/30d3d79b7d3607d56546dd2a6b49e156ba0ec634' (2022-03-25) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index c849bca1..e3c490fd 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1641205782, - "narHash": "sha256-4jY7RCWUoZ9cKD8co0/4tFARpWB+57+r1bLLvXNJliY=", + "lastModified": 1648199409, + "narHash": "sha256-JwPKdC2PoVBkG6E+eWw3j6BMR6sL3COpYWfif7RVb8Y=", "owner": "edolstra", "repo": "flake-compat", - "rev": "b7547d3eed6f32d06102ead8991ec52ab0a4f1a7", + "rev": "64a525ee38886ab9028e6f61790de0832aa3ef03", "type": "github" }, "original": { @@ -34,11 +34,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1646955661, - "narHash": "sha256-AYLta1PubJnrkv15+7G+6ErW5m9NcI9wSdJ+n7pKAe0=", + "lastModified": 1648219316, + "narHash": "sha256-Ctij+dOi0ZZIfX5eMhgwugfvB+WZSrvVNAyAuANOsnQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "e9545762b032559c27d8ec9141ed63ceca1aa1ac", + "rev": "30d3d79b7d3607d56546dd2a6b49e156ba0ec634", "type": "github" }, "original": { From 5f461374b8efac36d610fba74e3860b6675b629c Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Sat, 2 Apr 2022 17:25:49 +1000 Subject: [PATCH 190/605] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a114869c..eb4d1889 100644 --- a/README.md +++ b/README.md @@ -77,4 +77,4 @@ All launcher code is available under the GPL-3 license. [Source for the website](https://github.com/PolyMC/polymc.github.io) is hosted under the AGPL-3 License. -The logo and related assets are under the CC BY-NC-SA 4.0 license. +The logo and related assets are under the CC BY-SA 4.0 license. From 41d7b27d4376eaab1c4ed8dbe0fbd32000ac3e54 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 2 Apr 2022 13:29:37 +0200 Subject: [PATCH 191/605] fix: calculate world sizes individually --- launcher/minecraft/World.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index 9cf67ed9..dc756e06 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -196,7 +196,7 @@ int64_t calculateWorldSize(const QFileInfo &file) } else if(file.isDir()) { - QDirIterator it(file.absolutePath(), QDirIterator::Subdirectories); + QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); int64_t total = 0; while (it.hasNext()) { From d44fa416ca556de678def8d2846349ff190e641e Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Fri, 24 Dec 2021 15:00:36 +0000 Subject: [PATCH 192/605] Technic: Allow pack API urls to be used in search This mimics the behaviour that the Technic launcher has, and their website displays API URLs for. The big benefit of this, is to be able to install private packs now :) --- .../modplatform/technic/TechnicModel.cpp | 119 +++++++++++++----- .../pages/modplatform/technic/TechnicModel.h | 44 +++++-- 2 files changed, 124 insertions(+), 39 deletions(-) diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 0167f746..37db839c 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -1,16 +1,36 @@ -/* Copyright 2020-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2021 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 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 "TechnicModel.h" @@ -95,12 +115,21 @@ void Technic::ListModel::performSearch() QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { searchUrl = "https://api.technicpack.net/trending?build=multimc"; + searchMode = List; } - else - { + else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) { + searchUrl = QString("https://%1?build=multimc").arg(currentSearchTerm.mid(7)); + searchMode = Single; + } + else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { + searchUrl = QString("%1?build=multimc").arg(currentSearchTerm); + searchMode = Single; + } + else { searchUrl = QString( "https://api.technicpack.net/search?build=multimc&q=%1" ).arg(currentSearchTerm); + searchMode = List; } netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; @@ -125,26 +154,58 @@ void Technic::ListModel::searchRequestFinished() QList newList; try { auto root = Json::requireObject(doc); - auto objs = Json::requireArray(root, "modpacks"); - for (auto technicPack: objs) { - Modpack pack; - auto technicPackObject = Json::requireObject(technicPack); - pack.name = Json::requireString(technicPackObject, "name"); - pack.slug = Json::requireString(technicPackObject, "slug"); - if (pack.slug == "vanilla") - continue; - auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null"); - if(rawURL == "null") { - pack.logoUrl = "null"; - pack.logoName = "null"; + switch (searchMode) { + case List: { + auto objs = Json::requireArray(root, "modpacks"); + for (auto technicPack: objs) { + Modpack pack; + auto technicPackObject = Json::requireObject(technicPack); + pack.name = Json::requireString(technicPackObject, "name"); + pack.slug = Json::requireString(technicPackObject, "slug"); + if (pack.slug == "vanilla") + continue; + + auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null"); + if(rawURL == "null") { + pack.logoUrl = "null"; + pack.logoName = "null"; + } + else { + pack.logoUrl = rawURL; + pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + } + pack.broken = false; + newList.append(pack); + } + break; } - else { - pack.logoUrl = rawURL; - pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + case Single: { + if (root.contains("error")) { + // Invalid API url + break; + } + + Modpack pack; + pack.name = Json::requireString(root, "displayName"); + pack.slug = Json::requireString(root, "name"); + + if (root.contains("icon")) { + auto iconObj = Json::requireObject(root, "icon"); + auto iconUrl = Json::requireString(iconObj, "url"); + + pack.logoUrl = iconUrl; + pack.logoName = iconUrl.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + } + else { + pack.logoUrl = "null"; + pack.logoName = "null"; + } + + pack.broken = false; + newList.append(pack); + break; } - pack.broken = false; - newList.append(pack); } } catch (const JSONValidationError &err) diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h index e80e6e7c..5eea124c 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.h +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h @@ -1,16 +1,36 @@ -/* Copyright 2020-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2021 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 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 @@ -63,6 +83,10 @@ private: ResetRequested, Finished } searchState = None; + enum SearchMode { + List, + Single, + } searchMode = List; NetJob::Ptr jobPtr; QByteArray response; }; From f267375ac2d0086bf7cde7512b34ab324da375d4 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Fri, 24 Dec 2021 15:20:34 +0000 Subject: [PATCH 193/605] Technic: Prevent potential HTML injection --- launcher/ui/pages/modplatform/technic/TechnicPage.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index c3807269..25b6fd44 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -202,14 +202,12 @@ void TechnicPage::metadataLoaded() QString name = current.name; if (current.websiteUrl.isEmpty()) - // This allows injecting HTML here. - text = name; + text = name.toHtmlEscaped(); else - // URL not properly escaped for inclusion in HTML. The name allows for injecting HTML. - text = "" + name + ""; + text = "" + name.toHtmlEscaped() + ""; + if (!current.author.isEmpty()) { - // This allows injecting HTML here - text += tr(" by ") + current.author; + text += tr(" by ") + current.author.toHtmlEscaped(); } ui->frame->setModText(text); From 9d88f0795594f3f01d35bab4eb3767566b36c45c Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Fri, 24 Dec 2021 15:10:42 +0000 Subject: [PATCH 194/605] Technic: Include the modpack version in instance title --- launcher/ui/pages/modplatform/technic/TechnicData.h | 1 + launcher/ui/pages/modplatform/technic/TechnicPage.cpp | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/modplatform/technic/TechnicData.h b/launcher/ui/pages/modplatform/technic/TechnicData.h index 50fd75e8..4a374a6b 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicData.h +++ b/launcher/ui/pages/modplatform/technic/TechnicData.h @@ -36,6 +36,7 @@ struct Modpack { QString websiteUrl; QString author; QString description; + QString currentVersion; }; } diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 25b6fd44..c68021d8 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -189,6 +189,7 @@ void TechnicPage::suggestCurrent() 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.metadataLoaded = true; metadataLoaded(); }); @@ -214,11 +215,11 @@ void TechnicPage::metadataLoaded() ui->frame->setModDescription(current.description); if (!current.isSolder) { - dialog->setSuggestedPack(current.name, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); + dialog->setSuggestedPack(current.name + " " + current.currentVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); } else { while (current.url.endsWith('/')) current.url.chop(1); - dialog->setSuggestedPack(current.name, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, current.minecraftVersion)); + dialog->setSuggestedPack(current.name + " " + current.currentVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, current.minecraftVersion)); } } From c8092269baf92c05a8b2433fc644f1039a041bfa Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Mon, 27 Dec 2021 03:55:08 +0000 Subject: [PATCH 195/605] Technic: Match CurseForge pack description format --- launcher/ui/pages/modplatform/technic/TechnicPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index c68021d8..49682904 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -208,7 +208,7 @@ void TechnicPage::metadataLoaded() text = "" + name.toHtmlEscaped() + ""; if (!current.author.isEmpty()) { - text += tr(" by ") + current.author.toHtmlEscaped(); + text += "
" + tr(" by ") + current.author.toHtmlEscaped(); } ui->frame->setModText(text); From 8df88e7fbbbff42861a62c2fcd79c9e853781fc8 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Fri, 1 Apr 2022 23:40:20 +0100 Subject: [PATCH 196/605] Technic: Add API models for Solder packs --- launcher/CMakeLists.txt | 2 + .../technic/SolderPackManifest.cpp | 58 +++++++++++++++++++ .../modplatform/technic/SolderPackManifest.h | 49 ++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 launcher/modplatform/technic/SolderPackManifest.cpp create mode 100644 launcher/modplatform/technic/SolderPackManifest.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 692aebe5..83635512 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -539,6 +539,8 @@ set(TECHNIC_SOURCES modplatform/technic/SingleZipPackInstallTask.cpp modplatform/technic/SolderPackInstallTask.h modplatform/technic/SolderPackInstallTask.cpp + modplatform/technic/SolderPackManifest.h + modplatform/technic/SolderPackManifest.cpp modplatform/technic/TechnicPackProcessor.h modplatform/technic/TechnicPackProcessor.cpp ) diff --git a/launcher/modplatform/technic/SolderPackManifest.cpp b/launcher/modplatform/technic/SolderPackManifest.cpp new file mode 100644 index 00000000..16fe0b0e --- /dev/null +++ b/launcher/modplatform/technic/SolderPackManifest.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SolderPackManifest.h" + +#include "Json.h" + +namespace TechnicSolder { + +void loadPack(Pack& v, QJsonObject& obj) +{ + v.recommended = Json::requireString(obj, "recommended"); + v.latest = Json::requireString(obj, "latest"); + + auto builds = Json::requireArray(obj, "builds"); + for (const auto buildRaw : builds) { + auto build = Json::requireString(buildRaw); + v.builds.append(build); + } +} + +static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj) +{ + b.name = Json::requireString(obj, "name"); + b.version = Json::requireString(obj, "version"); + b.md5 = Json::requireString(obj, "md5"); + b.url = Json::requireString(obj, "url"); +} + +void loadPackBuild(PackBuild& v, QJsonObject& obj) +{ + v.minecraft = Json::requireString(obj, "minecraft"); + + auto mods = Json::requireArray(obj, "mods"); + for (const auto modRaw : mods) { + auto modObj = Json::requireObject(modRaw); + PackBuildMod mod; + loadPackBuildMod(mod, modObj); + v.mods.append(mod); + } +} + +} diff --git a/launcher/modplatform/technic/SolderPackManifest.h b/launcher/modplatform/technic/SolderPackManifest.h new file mode 100644 index 00000000..09f18df0 --- /dev/null +++ b/launcher/modplatform/technic/SolderPackManifest.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +namespace TechnicSolder { + +struct Pack { + QString recommended; + QString latest; + QVector builds; +}; + +void loadPack(Pack& v, QJsonObject& obj); + +struct PackBuildMod { + QString name; + QString version; + QString md5; + QString url; +}; + +struct PackBuild { + QString minecraft; + QVector mods; +}; + +void loadPackBuild(PackBuild& v, QJsonObject& obj); + +} From c8205fda9f927304312659d060ed3e7b2d0394aa Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sat, 2 Apr 2022 13:26:03 +0100 Subject: [PATCH 197/605] Technic: Replace inline parsing code with API models --- .../technic/SolderPackInstallTask.cpp | 120 +++++++++++------- 1 file changed, 77 insertions(+), 43 deletions(-) diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index b5c91582..aef87d78 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "SolderPackInstallTask.h" @@ -19,7 +39,9 @@ #include #include #include + #include "TechnicPackProcessor.h" +#include "SolderPackManifest.h" Technic::SolderPackInstallTask::SolderPackInstallTask( shared_qobject_ptr network, @@ -41,9 +63,11 @@ bool Technic::SolderPackInstallTask::abort() { void Technic::SolderPackInstallTask::executeTask() { - setStatus(tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString())); + setStatus(tr("Finding recommended version")); + m_filesNetJob = new NetJob(tr("Finding recommended version"), m_network); m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response)); + auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::versionSucceeded); connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); @@ -52,21 +76,29 @@ void Technic::SolderPackInstallTask::executeTask() void Technic::SolderPackInstallTask::versionSucceeded() { - try - { - QJsonDocument doc = Json::requireDocument(m_response); - QJsonObject obj = Json::requireObject(doc); - QString version = Json::requireString(obj, "recommended", "__placeholder__"); - m_sourceUrl = m_sourceUrl.toString() + '/' + version; + setStatus(tr("Resolving modpack files")); + + QJsonParseError parse_error {}; + QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << m_response; + return; } - catch (const JSONValidationError &e) - { - emitFailed(e.cause()); + auto obj = doc.object(); + + TechnicSolder::Pack pack; + try { + TechnicSolder::loadPack(pack, obj); + } + catch (const JSONValidationError& e) { + emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); m_filesNetJob.reset(); return; } - setStatus(tr("Resolving modpack files:\n%1").arg(m_sourceUrl.toString())); + m_sourceUrl = m_sourceUrl.toString() + '/' + pack.recommended; + m_filesNetJob = new NetJob(tr("Resolving modpack files"), m_network); m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response)); auto job = m_filesNetJob.get(); @@ -77,38 +109,41 @@ void Technic::SolderPackInstallTask::versionSucceeded() void Technic::SolderPackInstallTask::fileListSucceeded() { - setStatus(tr("Downloading modpack:")); - QStringList modUrls; - try - { - QJsonDocument doc = Json::requireDocument(m_response); - QJsonObject obj = Json::requireObject(doc); - QString minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__"); - if (!minecraftVersion.isEmpty()) - m_minecraftVersion = minecraftVersion; - QJsonArray mods = Json::requireArray(obj, "mods", "'mods'"); - for (auto mod: mods) - { - QJsonObject modObject = Json::requireObject(mod); - modUrls.append(Json::requireString(modObject, "url", "'url'")); - } + setStatus(tr("Downloading modpack")); + + QJsonParseError parse_error {}; + QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << m_response; + return; } - catch (const JSONValidationError &e) - { - emitFailed(e.cause()); + auto obj = doc.object(); + + TechnicSolder::PackBuild build; + try { + TechnicSolder::loadPackBuild(build, obj); + } + catch (const JSONValidationError& e) { + emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); m_filesNetJob.reset(); return; } + + if (!build.minecraft.isEmpty()) + m_minecraftVersion = build.minecraft; + m_filesNetJob = new NetJob(tr("Downloading modpack"), m_network); + int i = 0; - for (auto &modUrl: modUrls) + for (const auto &mod : build.mods) { auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); - m_filesNetJob->addNetAction(Net::Download::makeFile(modUrl, path)); + m_filesNetJob->addNetAction(Net::Download::makeFile(mod.url, path)); i++; } - m_modCount = modUrls.size(); + m_modCount = build.mods.size(); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged); @@ -206,6 +241,5 @@ void Technic::SolderPackInstallTask::extractFinished() void Technic::SolderPackInstallTask::extractAborted() { emitFailed(tr("Instance import has been aborted.")); - return; } From a232c2d50994d1e16b34ee84fbc24dcca80a4d9e Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Mon, 27 Dec 2021 03:52:54 +0000 Subject: [PATCH 198/605] Technic: Display available versions for Solder packs --- .../technic/SolderPackInstallTask.cpp | 53 ++----- .../technic/SolderPackInstallTask.h | 47 +++++-- .../pages/modplatform/technic/TechnicData.h | 45 ++++-- .../pages/modplatform/technic/TechnicPage.cpp | 131 +++++++++++++++-- .../pages/modplatform/technic/TechnicPage.h | 11 +- .../pages/modplatform/technic/TechnicPage.ui | 132 ++++++++---------- 6 files changed, 270 insertions(+), 149 deletions(-) diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index aef87d78..abdfcd9e 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2021-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 @@ -45,12 +45,16 @@ Technic::SolderPackInstallTask::SolderPackInstallTask( shared_qobject_ptr network, - const QUrl &sourceUrl, + const QUrl &solderUrl, + const QString &pack, + const QString &version, const QString &minecraftVersion ) { - m_sourceUrl = sourceUrl; - m_minecraftVersion = minecraftVersion; + m_solderUrl = solderUrl; + m_pack = pack; + m_version = version; m_network = network; + m_minecraftVersion = minecraftVersion; } bool Technic::SolderPackInstallTask::abort() { @@ -62,45 +66,13 @@ bool Technic::SolderPackInstallTask::abort() { } void Technic::SolderPackInstallTask::executeTask() -{ - setStatus(tr("Finding recommended version")); - - m_filesNetJob = new NetJob(tr("Finding recommended version"), m_network); - m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response)); - - auto job = m_filesNetJob.get(); - connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::versionSucceeded); - connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); - m_filesNetJob->start(); -} - -void Technic::SolderPackInstallTask::versionSucceeded() { setStatus(tr("Resolving modpack files")); - QJsonParseError parse_error {}; - QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << m_response; - return; - } - auto obj = doc.object(); - - TechnicSolder::Pack pack; - try { - TechnicSolder::loadPack(pack, obj); - } - catch (const JSONValidationError& e) { - emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); - m_filesNetJob.reset(); - return; - } - - m_sourceUrl = m_sourceUrl.toString() + '/' + pack.recommended; - m_filesNetJob = new NetJob(tr("Resolving modpack files"), m_network); - m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response)); + auto sourceUrl = QString("%1/modpack/%2/%3").arg(m_solderUrl.toString(), m_pack, m_version); + m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, &m_response)); + auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); @@ -136,8 +108,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded() m_filesNetJob = new NetJob(tr("Downloading modpack"), m_network); int i = 0; - for (const auto &mod : build.mods) - { + for (const auto &mod : build.mods) { auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); m_filesNetJob->addNetAction(Net::Download::makeFile(mod.url, path)); i++; diff --git a/launcher/modplatform/technic/SolderPackInstallTask.h b/launcher/modplatform/technic/SolderPackInstallTask.h index 9b2058d8..117a7bd6 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.h +++ b/launcher/modplatform/technic/SolderPackInstallTask.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2021-2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 @@ -27,7 +47,7 @@ namespace Technic { Q_OBJECT public: - explicit SolderPackInstallTask(shared_qobject_ptr network, const QUrl &sourceUrl, const QString &minecraftVersion); + explicit SolderPackInstallTask(shared_qobject_ptr network, const QUrl &solderUrl, const QString& pack, const QString& version, const QString &minecraftVersion); bool canAbort() const override { return true; } bool abort() override; @@ -37,7 +57,6 @@ namespace Technic virtual void executeTask() override; private slots: - void versionSucceeded(); void fileListSucceeded(); void downloadSucceeded(); void downloadFailed(QString reason); @@ -51,7 +70,9 @@ namespace Technic shared_qobject_ptr m_network; NetJob::Ptr m_filesNetJob; - QUrl m_sourceUrl; + QUrl m_solderUrl; + QString m_pack; + QString m_version; QString m_minecraftVersion; QByteArray m_response; QTemporaryDir m_outputDir; diff --git a/launcher/ui/pages/modplatform/technic/TechnicData.h b/launcher/ui/pages/modplatform/technic/TechnicData.h index 4a374a6b..cd2ea8e1 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicData.h +++ b/launcher/ui/pages/modplatform/technic/TechnicData.h @@ -1,22 +1,43 @@ -/* Copyright 2020-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2021-2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 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 namespace Technic { struct Modpack { @@ -37,6 +58,10 @@ struct Modpack { QString author; QString description; QString currentVersion; + + bool versionsLoaded = false; + QString recommended; + QVector versions; }; } diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 49682904..776361ed 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2021-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 @@ -46,6 +46,7 @@ #include "Json.h" #include "Application.h" +#include "modplatform/technic/SolderPackManifest.h" TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent) : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog) @@ -55,7 +56,9 @@ TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent) ui->searchEdit->installEventFilter(this); model = new Technic::ListModel(this); ui->packView->setModel(model); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &TechnicPage::onVersionSelectionChanged); } bool TechnicPage::eventFilter(QObject* watched, QEvent* event) @@ -98,13 +101,14 @@ void TechnicPage::triggerSearch() { void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second) { + ui->versionSelectionBox->clear(); + if(!first.isValid()) { if(isOpened) { dialog->setSuggestedPack(); } - //ui->frame->clear(); return; } @@ -137,17 +141,19 @@ void TechnicPage::suggestCurrent() } NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); - std::shared_ptr response = std::make_shared(); QString slug = current.slug; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get())); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, slug] + netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), &response)); + QObject::connect(netJob, &NetJob::succeeded, this, [this, slug] { + jobPtr.reset(); + if (current.slug != slug) { return; } - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + + QJsonParseError parse_error {}; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); QJsonObject obj = doc.object(); if(parse_error.error != QJsonParseError::NoError) { @@ -191,9 +197,12 @@ void TechnicPage::suggestCurrent() current.description = Json::ensureString(obj, "description", QString(), "__placeholder__"); current.currentVersion = Json::ensureString(obj, "version", QString(), "__placeholder__"); current.metadataLoaded = true; + metadataLoaded(); }); - netJob->start(); + + jobPtr = netJob; + jobPtr->start(); } // expects current.metadataLoaded to be true @@ -211,15 +220,111 @@ void TechnicPage::metadataLoaded() text += "
" + tr(" by ") + current.author.toHtmlEscaped(); } - ui->frame->setModText(text); - ui->frame->setModDescription(current.description); + text += "

"; + + ui->packDescription->setHtml(text + current.description); + + // Strip trailing forward-slashes from Solder URL's + if (current.isSolder) { + while (current.url.endsWith('/')) current.url.chop(1); + } + + // Display versions from Solder + if (!current.isSolder) { + // If the pack isn't a Solder pack, it only has the single version + ui->versionSelectionBox->addItem(current.currentVersion); + } + else if (current.versionsLoaded) { + // reverse foreach, so that the newest versions are first + for (auto i = current.versions.size(); i--;) { + ui->versionSelectionBox->addItem(current.versions.at(i)); + } + ui->versionSelectionBox->setCurrentText(current.recommended); + } + else { + // For now, until the versions are pulled from the Solder instance, display the current + // version so we can display something quicker + ui->versionSelectionBox->addItem(current.currentVersion); + + auto* netJob = new NetJob(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network()); + auto url = QString("%1/modpack/%2").arg(current.url, current.slug); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); + + QObject::connect(netJob, &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); + + jobPtr = netJob; + jobPtr->start(); + } + + selectVersion(); +} + +void TechnicPage::selectVersion() { + if (!isOpened) { + return; + } + if (current.broken) { + dialog->setSuggestedPack(); + return; + } + if (!current.isSolder) { - dialog->setSuggestedPack(current.name + " " + current.currentVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); + dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); } else { - while (current.url.endsWith('/')) current.url.chop(1); - dialog->setSuggestedPack(current.name + " " + current.currentVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, current.minecraftVersion)); + dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url, current.slug, selectedVersion, current.minecraftVersion)); } } + +void TechnicPage::onSolderLoaded() { + jobPtr.reset(); + + auto fallback = [this]() { + current.versionsLoaded = true; + + current.versions.clear(); + current.versions.append(current.currentVersion); + }; + + current.versions.clear(); + + QJsonParseError parse_error {}; + auto doc = QJsonDocument::fromJson(response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + fallback(); + return; + } + auto obj = doc.object(); + + TechnicSolder::Pack pack; + try { + TechnicSolder::loadPack(pack, obj); + } + catch (const JSONValidationError& err) { + qCritical() << "Couldn't parse Solder pack metadata:" << err.cause(); + fallback(); + return; + } + + current.versionsLoaded = true; + current.recommended = pack.recommended; + current.versions.append(pack.builds); + + // Finally, let's reload :) + ui->versionSelectionBox->clear(); + metadataLoaded(); +} + +void TechnicPage::onVersionSelectionChanged(QString data) { + if (data.isNull() || data.isEmpty()) { + selectedVersion = ""; + return; + } + + selectedVersion = data; + selectVersion(); +} diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index bf4baa58..f4a3b61d 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2021-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 @@ -39,6 +39,7 @@ #include "ui/pages/BasePage.h" #include +#include "net/NetJob.h" #include "tasks/Task.h" #include "TechnicData.h" @@ -86,14 +87,22 @@ public: private: void suggestCurrent(); void metadataLoaded(); + void selectVersion(); private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); + void onSolderLoaded(); + void onVersionSelectionChanged(QString data); private: Ui::TechnicPage *ui = nullptr; NewInstanceDialog* dialog = nullptr; Technic::ListModel* model = nullptr; + Technic::Modpack current; + QString selectedVersion; + + NetJob::Ptr jobPtr; + QByteArray response; }; diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index 62ab6154..ca6a9b7e 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -10,86 +10,76 @@ 405
- - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Search and filter... - - - - - - - Search - - - - - + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 1 + 1 + + + + + - - - - Qt::ScrollBarAlwaysOff - - - true - - - - 48 - 48 - + + + + + + true + + + + 48 + 48 + + + + + + + + + + + + + Search and filter... - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised + + + + Search
- - - MCModInfoFrame - QFrame -
ui/widgets/MCModInfoFrame.h
- 1 -
-
- - searchEdit - searchButton - packView - From 7f2615b2a5473064472cde9d5da739e6a27667ab Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sat, 2 Apr 2022 00:11:33 +0100 Subject: [PATCH 199/605] Technic: Verify checksums for pack build mods --- launcher/modplatform/technic/SolderPackInstallTask.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index abdfcd9e..89dbf4ca 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -42,6 +42,7 @@ #include "TechnicPackProcessor.h" #include "SolderPackManifest.h" +#include "net/ChecksumValidator.h" Technic::SolderPackInstallTask::SolderPackInstallTask( shared_qobject_ptr network, @@ -110,7 +111,14 @@ void Technic::SolderPackInstallTask::fileListSucceeded() int i = 0; for (const auto &mod : build.mods) { auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); - m_filesNetJob->addNetAction(Net::Download::makeFile(mod.url, path)); + + auto dl = Net::Download::makeFile(mod.url, path); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } + m_filesNetJob->addNetAction(dl); + i++; } From b6e722a048aabd35d6e2d8db3ad26e50b1f5a7fb Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sun, 9 Jan 2022 23:02:32 +0000 Subject: [PATCH 200/605] BuildConfig: Make Technic API base URL and build constants --- buildconfig/BuildConfig.h | 6 ++++++ .../ui/pages/modplatform/technic/TechnicModel.cpp | 13 ++++++++----- .../ui/pages/modplatform/technic/TechnicPage.cpp | 3 ++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 2fb71f14..6304387c 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -140,6 +140,12 @@ public: QString ATL_DOWNLOAD_SERVER_URL = "https://download.nodecdn.net/containers/atl/"; + QString TECHNIC_API_BASE_URL = "https://api.technicpack.net/"; + /** + * The build that is reported to the Technic API. + */ + QString TECHNIC_API_BUILD = "multimc"; + /** * \brief Converts the Version to a string. * \return The version number in string format (major.minor.revision.build). diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 37db839c..9c9d1e75 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -35,6 +35,7 @@ #include "TechnicModel.h" #include "Application.h" +#include "BuildConfig.h" #include "Json.h" #include @@ -114,21 +115,23 @@ void Technic::ListModel::performSearch() NetJob *netJob = new NetJob("Technic::Search", APPLICATION->network()); QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { - searchUrl = "https://api.technicpack.net/trending?build=multimc"; + searchUrl = QString("%1trending?build=%2") + .arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD); searchMode = List; } else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) { - searchUrl = QString("https://%1?build=multimc").arg(currentSearchTerm.mid(7)); + searchUrl = QString("https://%1?build=%2") + .arg(currentSearchTerm.mid(7), BuildConfig.TECHNIC_API_BUILD); searchMode = Single; } else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { - searchUrl = QString("%1?build=multimc").arg(currentSearchTerm); + searchUrl = QString("%1?build=%2").arg(currentSearchTerm, BuildConfig.TECHNIC_API_BUILD); searchMode = Single; } else { searchUrl = QString( - "https://api.technicpack.net/search?build=multimc&q=%1" - ).arg(currentSearchTerm); + "%1search?build=%2&q=%3" + ).arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); searchMode = List; } netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 776361ed..b8c1e00a 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -40,6 +40,7 @@ #include "ui/dialogs/NewInstanceDialog.h" +#include "BuildConfig.h" #include "TechnicModel.h" #include "modplatform/technic/SingleZipPackInstallTask.h" #include "modplatform/technic/SolderPackInstallTask.h" @@ -142,7 +143,7 @@ void TechnicPage::suggestCurrent() NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); QString slug = current.slug; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), &response)); + netJob->addNetAction(Net::Download::makeByteArray(QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), &response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, slug] { jobPtr.reset(); From 02b44256b29fd10499177459e7ff515d7e81c10a Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Sun, 3 Apr 2022 20:37:50 +1000 Subject: [PATCH 201/605] Fix matrix links and add reddit --- README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a114869c..556dae61 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,22 @@ Feel free to create an issue if you need help. However, you might find it easier For people who don't want to use Discord, we have a Matrix Space which is bridged to the Discord server: -[![PolyMC Space](https://img.shields.io/matrix/polymc:polymc.org?label=PolyMC%20Space&server_fqdn=matrix.polymc.org)](https://matrix.to/#/#polymc:polymc.org) +[![PolyMC Space](https://img.shields.io/matrix/polymc:matrix.org?label=PolyMC%20space)](https://matrix.to/#/#polymc:matrix.org) If there are any issues with the space or you are using a client that does not support the feature here are the individual rooms: -[![Support](https://img.shields.io/matrix/support:polymc.org?label=%23support&server_fqdn=matrix.polymc.org)](https://matrix.to/#/#support:polymc.org) -[![Discussion](https://img.shields.io/matrix/discussion:polymc.org?label=%23discussion&server_fqdn=matrix.polymc.org)](https://matrix.to/#/#discussion:polymc.org) -[![Development](https://img.shields.io/matrix/development:polymc.org?label=%23development&server_fqdn=matrix.polymc.org)](https://matrix.to/#/#development:polymc.org) -[![News](https://img.shields.io/matrix/news:polymc.org?label=%23news&server_fqdn=matrix.polymc.org)](https://matrix.to/#/#news:polymc.org) +[![Development](https://img.shields.io/matrix/polymc-development:matrix.org?label=PolyMC%20Development)](https://matrix.to/#/#polymc-development:matrix.org) +[![Discussion](https://img.shields.io/matrix/polymc-discussion:matrix.org?label=PolyMC%20Discussion)](https://matrix.to/#/#polymc-discussion:matrix.org) +[![Github](https://img.shields.io/matrix/polymc-github:matrix.org?label=PolyMC%20Github)](https://matrix.to/#/#polymc-github:matrix.org) +[![Maintainers](https://img.shields.io/matrix/polymc-maintainers:matrix.org?label=PolyMC%20Maintainers)](https://matrix.to/#/#polymc-maintainers:matrix.org) +[![News](https://img.shields.io/matrix/polymc-news:matrix.org?label=PolyMC%20News)](https://matrix.to/#/#polymc-news:matrix.org) +[![Offtopic](https://img.shields.io/matrix/polymc-offtopic:matrix.org?label=PolyMC%20Offtopic)](https://matrix.to/#/#polymc-offtopic:matrix.org) +[![Support](https://img.shields.io/matrix/polymc-support:matrix.org?label=PolyMC%20Support)](https://matrix.to/#/#polymc-support:matrix.org) +[![Voice](https://img.shields.io/matrix/polymc-voice:matrix.org?label=PolyMC%20Voice)](https://matrix.to/#/#polymc-voice:matrix.org) + +we also have a subreddit you can post your issues and suggestions on: + +[r/PolyMCLauncher](https://www.reddit.com/r/PolyMCLauncher/) # Development From c367769781cba323afa3f4185a333ca7f7a7a72a Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Sun, 3 Apr 2022 20:39:44 +1000 Subject: [PATCH 202/605] Update CMakeLists.txt --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b97635c1..d45d4975 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,7 +80,7 @@ set(Launcher_BUG_TRACKER_URL "https://github.com/PolyMC/PolyMC/issues" CACHE STR set(Launcher_TRANSLATIONS_URL "https://hosted.weblate.org/projects/polymc/polymc/" CACHE STRING "URL for the translations platform.") # Matrix Space -set(Launcher_MATRIX_URL "https://matrix.to/#/#polymc:polymc.org" CACHE STRING "URL to the Matrix Space") +set(Launcher_MATRIX_URL "https://matrix.to/#/#polymc:matrix.org" CACHE STRING "URL to the Matrix Space") # Discord URL set(Launcher_DISCORD_URL "https://discord.gg/Z52pwxWCHP" CACHE STRING "URL for the Discord guild.") @@ -88,7 +88,7 @@ set(Launcher_DISCORD_URL "https://discord.gg/Z52pwxWCHP" CACHE STRING "URL for t # Subreddit URL -set(Launcher_SUBREDDIT_URL "" CACHE STRING "URL for the subreddit.") +set(Launcher_SUBREDDIT_URL "https://www.reddit.com/r/PolyMCLauncher/" CACHE STRING "URL for the subreddit.") # Builds # TODO: Launcher_FORCE_BUNDLED_LIBS should be off in the future, but as of QuaZip 1.2, we can't do that yet. From d2ffaee9f81c97b7249f28a5c0a1c69e376d2ae1 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sun, 3 Apr 2022 12:10:43 +0200 Subject: [PATCH 203/605] remove deadcode in CI --- .github/workflows/build.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b011a779..c2571e91 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,13 +16,8 @@ jobs: include: - os: ubuntu-20.04 - qt_version: 5.12.8 - qt_host: linux - os: ubuntu-20.04 - name: Linux-Portable - qt_version: 5.12.8 - qt_host: linux portable: true - os: ubuntu-20.04 @@ -33,12 +28,10 @@ jobs: - os: windows-2022 name: "Windows-i686" msystem: mingw32 - portable: false - os: windows-2022 name: "Windows-x86_64" msystem: mingw64 - portable: false - os: windows-2022 name: "Windows-i686-portable" From bd8b61651a1df2fc1e5c6c43a6d03d2c2d43d5b7 Mon Sep 17 00:00:00 2001 From: Harry Peach Date: Sun, 3 Apr 2022 23:06:44 +0100 Subject: [PATCH 204/605] Check for empty slug before setting pack url --- launcher/modplatform/modrinth/ModrinthPackIndex.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 5b75f034..a3c2f166 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -12,7 +12,13 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireString(obj, "project_id"); pack.name = Json::requireString(obj, "title"); - pack.websiteUrl = "https://modrinth.com/mod/" + Json::ensureString(obj, "slug", ""); + + QString slug = Json::ensureString(obj, "slug", ""); + if (!slug.isEmpty()) + pack.websiteUrl = "https://modrinth.com/mod/" + Json::ensureString(obj, "slug", ""); + else + pack.websiteUrl = ""; + pack.description = Json::ensureString(obj, "description", ""); pack.logoUrl = Json::requireString(obj, "icon_url"); From cf8680f1ab3eb18da8f9f2235010065a51313061 Mon Sep 17 00:00:00 2001 From: Victor Date: Sat, 19 Feb 2022 15:47:34 +0000 Subject: [PATCH 205/605] fix: properly detect arm64 --- launcher/java/JavaChecker.cpp | 2 +- launcher/java/JavaInstallList.cpp | 2 +- launcher/launch/steps/CheckJava.cpp | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index 35ddc35c..946599c5 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -129,7 +129,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) auto os_arch = results["os.arch"]; auto java_version = results["java.version"]; auto java_vendor = results["java.vendor"]; - bool is_64 = os_arch == "x86_64" || os_arch == "amd64"; + bool is_64 = os_arch == "x86_64" || os_arch == "amd64" || os_arch == "aarch64" || os_arch == "arm64"; result.validity = JavaCheckResult::Validity::Valid; diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp index a0a60871..9b745095 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -183,7 +183,7 @@ void JavaListLoadTask::javaCheckerFinished() JavaInstallPtr javaVersion(new JavaInstall()); javaVersion->id = result.javaVersion; - javaVersion->arch = result.mojangPlatform; + javaVersion->arch = result.realPlatform; javaVersion->path = result.path; candidates.append(javaVersion); diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp index c2ebb334..3226fae7 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -124,7 +124,8 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) case JavaCheckResult::Validity::Valid: { auto instance = m_parent->instance(); - printJavaInfo(result.javaVersion.toString(), result.mojangPlatform, result.javaVendor); + printJavaInfo(result.javaVersion.toString(), result.realPlatform, result.javaVendor); + printSystemInfo(true, result.is_64bit); instance->settings()->set("JavaVersion", result.javaVersion.toString()); instance->settings()->set("JavaArchitecture", result.mojangPlatform); instance->settings()->set("JavaVendor", result.javaVendor); @@ -137,8 +138,7 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) void CheckJava::printJavaInfo(const QString& version, const QString& architecture, const QString & vendor) { - emit logLine(QString("Java is version %1, using %2-bit architecture, from %3.\n\n").arg(version, architecture, vendor), MessageLevel::Launcher); - printSystemInfo(true, architecture == "64"); + emit logLine(QString("Java is version %1, using %2 architecture, from %3.\n\n").arg(version, architecture, vendor), MessageLevel::Launcher); } void CheckJava::printSystemInfo(bool javaIsKnown, bool javaIs64bit) From dc6340bf384d6f54f9f2793c55235b1bdd879b00 Mon Sep 17 00:00:00 2001 From: Una Date: Tue, 5 Apr 2022 23:22:24 -0700 Subject: [PATCH 206/605] Allow components to specify Java agents and JVM arguments (#175) --- launcher/CMakeLists.txt | 2 +- launcher/minecraft/Agent.h | 36 +++++++++++++++++++++ launcher/minecraft/LaunchProfile.cpp | 33 +++++++++++++++++++ launcher/minecraft/LaunchProfile.h | 14 ++++++++ launcher/minecraft/MinecraftInstance.cpp | 11 +++++++ launcher/minecraft/OneSixVersionFormat.cpp | 24 ++++++++++++++ launcher/minecraft/VersionFile.cpp | 5 +++ launcher/minecraft/VersionFile.h | 7 ++++ launcher/minecraft/update/LibrariesTask.cpp | 4 +++ launcher/ui/widgets/CustomCommands.ui | 2 +- 10 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 launcher/minecraft/Agent.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 692aebe5..13085c4c 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -348,7 +348,7 @@ set(MINECRAFT_SOURCES mojang/PackageManifest.h mojang/PackageManifest.cpp - ) + minecraft/Agent.h) add_unit_test(GradleSpecifier SOURCES minecraft/GradleSpecifier_test.cpp diff --git a/launcher/minecraft/Agent.h b/launcher/minecraft/Agent.h new file mode 100644 index 00000000..01109daf --- /dev/null +++ b/launcher/minecraft/Agent.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "Library.h" + +class Agent; + +typedef std::shared_ptr AgentPtr; + +class Agent { +public: + Agent(LibraryPtr library, QString &argument) + { + m_library = library; + m_argument = argument; + } + +public: /* methods */ + + LibraryPtr library() { + return m_library; + } + QString argument() { + return m_argument; + } + +protected: /* data */ + + /// The library pointing to the jar this Java agent is contained within + LibraryPtr m_library; + + /// The argument to the Java agent, passed after an = if present + QString m_argument; + +}; diff --git a/launcher/minecraft/LaunchProfile.cpp b/launcher/minecraft/LaunchProfile.cpp index cd77aa4a..39a342ca 100644 --- a/launcher/minecraft/LaunchProfile.cpp +++ b/launcher/minecraft/LaunchProfile.cpp @@ -42,11 +42,13 @@ void LaunchProfile::clear() m_minecraftVersionType.clear(); m_minecraftAssets.reset(); m_minecraftArguments.clear(); + m_addnJvmArguments.clear(); m_tweakers.clear(); m_mainClass.clear(); m_appletClass.clear(); m_libraries.clear(); m_mavenFiles.clear(); + m_agents.clear(); m_traits.clear(); m_jarMods.clear(); m_mainJar.reset(); @@ -80,6 +82,11 @@ void LaunchProfile::applyMinecraftArguments(const QString& minecraftArguments) applyString(minecraftArguments, this->m_minecraftArguments); } +void LaunchProfile::applyAddnJvmArguments(const QStringList& addnJvmArguments) +{ + this->m_addnJvmArguments.append(addnJvmArguments); +} + void LaunchProfile::applyMinecraftVersionType(const QString& type) { applyString(type, this->m_minecraftVersionType); @@ -214,6 +221,22 @@ void LaunchProfile::applyMavenFile(LibraryPtr mavenFile) m_mavenFiles.append(Library::limitedCopy(mavenFile)); } +void LaunchProfile::applyAgent(AgentPtr agent) +{ + auto lib = agent->library(); + if(!lib->isActive()) + { + return; + } + + if(lib->isNative()) + { + return; + } + + m_agents.append(agent); +} + const LibraryPtr LaunchProfile::getMainJar() const { return m_mainJar; @@ -295,6 +318,11 @@ QString LaunchProfile::getMinecraftArguments() const return m_minecraftArguments; } +const QStringList & LaunchProfile::getAddnJvmArguments() const +{ + return m_addnJvmArguments; +} + const QList & LaunchProfile::getJarMods() const { return m_jarMods; @@ -315,6 +343,11 @@ const QList & LaunchProfile::getMavenFiles() const return m_mavenFiles; } +const QList & LaunchProfile::getAgents() const +{ + return m_agents; +} + const QList & LaunchProfile::getCompatibleJavaMajors() const { return m_compatibleJavaMajors; diff --git a/launcher/minecraft/LaunchProfile.h b/launcher/minecraft/LaunchProfile.h index 366ed805..b55cf661 100644 --- a/launcher/minecraft/LaunchProfile.h +++ b/launcher/minecraft/LaunchProfile.h @@ -36,6 +36,7 @@ #pragma once #include #include "Library.h" +#include "Agent.h" #include class LaunchProfile: public ProblemProvider @@ -48,6 +49,7 @@ public: /* application of profile variables from patches */ void applyMainClass(const QString& mainClass); void applyAppletClass(const QString& appletClass); void applyMinecraftArguments(const QString& minecraftArguments); + void applyAddnJvmArguments(const QStringList& minecraftArguments); void applyMinecraftVersionType(const QString& type); void applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets); void applyTraits(const QSet &traits); @@ -56,6 +58,7 @@ public: /* application of profile variables from patches */ void applyMods(const QList &jarMods); void applyLibrary(LibraryPtr library); void applyMavenFile(LibraryPtr library); + void applyAgent(AgentPtr agent); void applyCompatibleJavaMajors(QList& javaMajor); void applyMainJar(LibraryPtr jar); void applyProblemSeverity(ProblemSeverity severity); @@ -69,12 +72,14 @@ public: /* getters for profile variables */ QString getMinecraftVersionType() const; MojangAssetIndexInfo::Ptr getMinecraftAssets() const; QString getMinecraftArguments() const; + const QStringList & getAddnJvmArguments() const; const QSet & getTraits() const; const QStringList & getTweakers() const; const QList & getJarMods() const; const QList & getLibraries() const; const QList & getNativeLibraries() const; const QList & getMavenFiles() const; + const QList & getAgents() const; const QList & getCompatibleJavaMajors() const; const LibraryPtr getMainJar() const; void getLibraryFiles( @@ -106,6 +111,12 @@ private: */ QString m_minecraftArguments; + /** + * Additional arguments to pass to the JVM in addition to those the user has configured, + * memory settings, etc. + */ + QStringList m_addnJvmArguments; + /// A list of all tweaker classes QStringList m_tweakers; @@ -121,6 +132,9 @@ private: /// the list of maven files to be placed in the libraries folder, but not acted upon QList m_mavenFiles; + /// the list of java agents to add to JVM arguments + QList m_agents; + /// the main jar LibraryPtr m_mainJar; diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index fd933df7..3ba79178 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -330,6 +330,17 @@ QStringList MinecraftInstance::extraArguments() const list.append({"-Dfml.ignoreInvalidMinecraftCertificates=true", "-Dfml.ignorePatchDiscrepancies=true"}); } + auto addn = m_components->getProfile()->getAddnJvmArguments(); + if (!addn.isEmpty()) { + list.append(addn); + } + auto agents = m_components->getProfile()->getAgents(); + for (auto agent : agents) + { + QStringList jar, temp1, temp2, temp3; + agent->library()->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, getLocalLibraryPath()); + list.append("-javaagent:"+jar[0]+(agent->argument().isEmpty() ? "" : "="+agent->argument())); + } return list; } diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index 0329d70e..879f18c1 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -1,5 +1,6 @@ #include "OneSixVersionFormat.h" #include +#include "minecraft/Agent.h" #include "minecraft/ParseUtils.h" #include @@ -108,6 +109,14 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc } } + if (root.contains("+jvmArgs")) + { + for (auto arg : requireArray(root.value("+jvmArgs"))) + { + out->addnJvmArguments.append(requireString(arg)); + } + } + if (root.contains("jarMods")) { @@ -176,6 +185,21 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc readLibs("mavenFiles", out->mavenFiles); } + if(root.contains("+agents")) { + for (auto agentVal : requireArray(root.value("+agents"))) + { + QJsonObject agentObj = requireObject(agentVal); + auto lib = libraryFromJson(*out, agentObj, filename); + QString arg = ""; + if (agentObj.contains("argument")) + { + readString(agentObj, "argument", arg); + } + AgentPtr agent(new Agent(lib, arg)); + out->agents.append(agent); + } + } + // if we have mainJar, just use it if(root.contains("mainJar")) { diff --git a/launcher/minecraft/VersionFile.cpp b/launcher/minecraft/VersionFile.cpp index 94fb6db7..9db30ba2 100644 --- a/launcher/minecraft/VersionFile.cpp +++ b/launcher/minecraft/VersionFile.cpp @@ -67,6 +67,7 @@ void VersionFile::applyTo(LaunchProfile *profile) profile->applyMainClass(mainClass); profile->applyAppletClass(appletClass); profile->applyMinecraftArguments(minecraftArguments); + profile->applyAddnJvmArguments(addnJvmArguments); profile->applyTweakers(addTweakers); profile->applyJarMods(jarMods); profile->applyMods(mods); @@ -81,6 +82,10 @@ void VersionFile::applyTo(LaunchProfile *profile) { profile->applyMavenFile(mavenFile); } + for (auto agent : agents) + { + profile->applyAgent(agent); + } profile->applyProblemSeverity(getProblemSeverity()); } diff --git a/launcher/minecraft/VersionFile.h b/launcher/minecraft/VersionFile.h index a7a19c4e..d4b29719 100644 --- a/launcher/minecraft/VersionFile.h +++ b/launcher/minecraft/VersionFile.h @@ -45,6 +45,7 @@ #include "minecraft/Rule.h" #include "ProblemProvider.h" #include "Library.h" +#include "Agent.h" #include class PackProfile; @@ -92,6 +93,9 @@ public: /* data */ /// Mojang: Minecraft launch arguments (may contain placeholders for variable substitution) QString minecraftArguments; + /// PolyMC: Additional JVM launch arguments + QStringList addnJvmArguments; + /// Mojang: list of compatible java majors QList compatibleJavaMajors; @@ -116,6 +120,9 @@ public: /* data */ /// PolyMC: list of maven files to put in the libraries folder, but not in classpath QList mavenFiles; + /// PolyMC: list of agents to add to JVM arguments + QList agents; + /// The main jar (Minecraft version library, normally) LibraryPtr mainJar; diff --git a/launcher/minecraft/update/LibrariesTask.cpp b/launcher/minecraft/update/LibrariesTask.cpp index 667dd5d9..26679110 100644 --- a/launcher/minecraft/update/LibrariesTask.cpp +++ b/launcher/minecraft/update/LibrariesTask.cpp @@ -48,6 +48,10 @@ void LibrariesTask::executeTask() libArtifactPool.append(profile->getLibraries()); libArtifactPool.append(profile->getNativeLibraries()); libArtifactPool.append(profile->getMavenFiles()); + for (auto agent : profile->getAgents()) + { + libArtifactPool.append(agent->library()); + } libArtifactPool.append(profile->getMainJar()); processArtifactPool(libArtifactPool, failedLocalLibraries, inst->getLocalLibraryPath()); diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index dbd54431..650a9cc1 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -74,7 +74,7 @@ - <html><head/><body><p>Pre-launch command runs before the instance launches and post-exit command runs after it exits.</p><p>Both will be run in the launcher's working folder with extra environment variables:</p><ul><li>$INST_NAME - Name of the instance</li><li>$INST_ID - ID of the instance (its folder name)</li><li>$INST_DIR - absolute path of the instance</li><li>$INST_MC_DIR - absolute path of Minecraft</li><li>$INST_JAVA - Java binary used for launch</li><li>$INST_JAVA_ARGS - command-line parameters used for launch</li></ul><p>Wrapper command allows launching using an extra wrapper program (like 'optirun' on Linux)</p></body></html> + <html><head/><body><p>Pre-launch command runs before the instance launches and post-exit command runs after it exits.</p><p>Both will be run in the launcher's working folder with extra environment variables:</p><ul><li>$INST_NAME - Name of the instance</li><li>$INST_ID - ID of the instance (its folder name)</li><li>$INST_DIR - absolute path of the instance</li><li>$INST_MC_DIR - absolute path of Minecraft</li><li>$INST_JAVA - Java binary used for launch</li><li>$INST_JAVA_ARGS - command-line parameters used for launch (warning: will not work correctly if arguments contain spaces)</li></ul><p>Wrapper command allows launching using an extra wrapper program (like 'optirun' on Linux)</p></body></html> Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop From 9eb9ddc6680244f2c10fa3ac50fbbeffefd2db29 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 20 Feb 2022 17:51:26 +0100 Subject: [PATCH 207/605] feat: initial Quilt support --- launcher/minecraft/ComponentUpdateTask.cpp | 9 +++++++ launcher/ui/pages/instance/VersionPage.cpp | 30 ++++++++++++++++++++++ launcher/ui/pages/instance/VersionPage.h | 1 + launcher/ui/pages/instance/VersionPage.ui | 9 +++++++ 4 files changed, 49 insertions(+) diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index 8bc05a1b..a856662a 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -600,6 +600,15 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) component->m_version = (*minecraft)->getVersion(); } } + else if (add.uid == "org.quiltmc.quilt-mappings") + { + auto minecraft = std::find_if(components.begin(), components.end(), [](ComponentPtr & cmp){ + return cmp->getID() == "net.minecraft"; + }); + if(minecraft != components.end()) { + component->m_version = (*minecraft)->getVersion() + "+build.1"; + } + } } // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. // ############################################################################################################ diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index ed37dd1a..c8857017 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -243,6 +243,9 @@ void VersionPage::updateVersionControls() bool supportsFabric = minecraftVersion >= Version("1.14"); ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric); + bool supportsQuilt = minecraftVersion >= Version("1.17.1"); + ui->actionInstall_Quilt->setEnabled(controlsEnabled && supportsQuilt); + bool supportsLiteLoader = minecraftVersion <= Version("1.12.2"); ui->actionInstall_LiteLoader->setEnabled(controlsEnabled && supportsLiteLoader); @@ -498,6 +501,33 @@ void VersionPage::on_actionInstall_Fabric_triggered() } } +void VersionPage::on_actionInstall_Quilt_triggered() +{ + auto vlist = APPLICATION->metadataIndex()->get("org.quiltmc.quilt-loader"); + if(!vlist) + { + return; + } + VersionSelectDialog vselect(vlist.get(), tr("Select Quilt Loader version"), this); + vselect.setEmptyString(tr("No Quilt Loader versions are currently available.")); + vselect.setEmptyErrorString(tr("Couldn't load or download the Quilt Loader version lists!")); + + auto currentVersion = m_profile->getComponentVersion("org.quiltmc.quilt-loader"); + if(!currentVersion.isEmpty()) + { + vselect.setCurrentVersion(currentVersion); + } + + if (vselect.exec() && vselect.selectedVersion()) + { + auto vsn = vselect.selectedVersion(); + m_profile->setComponentVersion("org.quiltmc.quilt-loader", vsn->descriptor()); + m_profile->resolve(Net::Mode::Online); + preselect(m_profile->rowCount(QModelIndex())-1); + m_container->refreshContainer(); + } +} + void VersionPage::on_actionAdd_Empty_triggered() { NewComponentDialog compdialog(QString(), QString(), this); diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index 2d37af43..979311fc 100644 --- a/launcher/ui/pages/instance/VersionPage.h +++ b/launcher/ui/pages/instance/VersionPage.h @@ -73,6 +73,7 @@ private slots: void on_actionChange_version_triggered(); void on_actionInstall_Forge_triggered(); void on_actionInstall_Fabric_triggered(); + void on_actionInstall_Quilt_triggered(); void on_actionAdd_Empty_triggered(); void on_actionInstall_LiteLoader_triggered(); void on_actionReload_triggered(); diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui index a4990ff3..489f7218 100644 --- a/launcher/ui/pages/instance/VersionPage.ui +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -107,6 +107,7 @@ + @@ -192,6 +193,14 @@ Install the Fabric Loader package. + + + Install Quilt + + + Install the Quilt Loader package. + + Install LiteLoader From 566a83b245b347e3bef72153c2a68dbbf5233ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Wed, 6 Apr 2022 22:45:57 +0200 Subject: [PATCH 208/605] NOISSUE prevent -version being passed to the JRE We want specific JREs, always, not something that is magically resolved. This counteracts some really bad advice recently being spread on reddit. --- launcher/JavaCommon.cpp | 11 +++++++++++ launcher/LaunchController.cpp | 5 ++++- launcher/ui/pages/instance/InstanceSettingsPage.cpp | 1 - 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index a6542fa7..ce16e8de 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -17,6 +17,17 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent) QMessageBox::Warning)->exec(); return false; } + // block lunacy with passing required version to the JVM + if (jvmargs.contains(QRegExp("-version:.*"))) { + auto warnStr = QObject::tr( + "You tried to pass required java version argument to the JVM (using \"-version=xxx\"). This is not safe and will not be allowed.\n" + "This message will be displayed until you remove this from the JVM arguments."); + CustomMessageBox::selectable( + parent, QObject::tr("JVM arguments warning"), + warnStr, + QMessageBox::Warning)->exec(); + return false; + } return true; } diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 792d8381..cc1228f5 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -71,7 +71,10 @@ void LaunchController::executeTask() return; } - JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget); + if(!JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget)) { + emitFailed(tr("Invalid Java arguments specified. Please fix this first.")); + return; + } login(); } diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index a5985741..a48c4d69 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -179,7 +179,6 @@ void InstanceSettingsPage::applySettings() if(javaArgs) { m_settings->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); - JavaCommon::checkJVMArgs(m_settings->get("JvmArgs").toString(), this->parentWidget()); } else { From e6564aa69fe60e41a77fc10a7c98484d5c0cd488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Fri, 18 Mar 2022 17:59:11 +0100 Subject: [PATCH 209/605] NOISSUE fix error string for Xbox authorization failures --- launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp index 07eeb7dc..589768e3 100644 --- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -65,7 +65,7 @@ void XboxAuthorizationStep::onRequestDone( if(!processSTSError(error, data, headers)) { emit finished( AccountTaskState::STATE_FAILED_SOFT, - tr("Failed to get authorization for %1 services. Error %1.").arg(m_authorizationKind, error) + tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, error) ); } return; From 1811302debffb18e923871752750573092d4ed22 Mon Sep 17 00:00:00 2001 From: Vladislav Laetansky Date: Mon, 28 Mar 2022 15:40:34 +0300 Subject: [PATCH 210/605] NOISSUE save custom offline player name --- launcher/Application.cpp | 3 +++ launcher/LaunchController.cpp | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 690a7ee4..9cca534c 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -696,6 +696,9 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Minecraft launch method m_settings->registerSetting("MCLaunchMethod", "LauncherPart"); + // Minecraft offline player name + m_settings->registerSetting("LastOfflinePlayerName", ""); + // Wrapper command for launch m_settings->registerSetting("WrapperCommand", ""); diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index cc1228f5..4cb62e69 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -169,13 +169,14 @@ void LaunchController::login() { if(!m_session->wants_online) { // we ask the user for a player name bool ok = false; - QString usedname = m_session->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"), tr("Choose your offline mode player name."), QLineEdit::Normal, - m_session->player_name, + usedname, &ok ); if (!ok) @@ -186,6 +187,7 @@ void LaunchController::login() { if (name.length()) { usedname = name; + APPLICATION->settings()->set("LastOfflinePlayerName", usedname); } m_session->MakeOffline(usedname); // offline flavored game from here :3 From 9349232bd4e6879ab11a3e8aaf5b3e056fdca2d9 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 20 Feb 2022 20:22:12 +0100 Subject: [PATCH 211/605] refactor: dynamically get best version for intermediary mappings --- launcher/minecraft/ComponentUpdateTask.cpp | 51 +++++++++++++--------- launcher/minecraft/ComponentUpdateTask.h | 2 + 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index a856662a..a0559232 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -2,7 +2,6 @@ #include "PackProfile_p.h" #include "PackProfile.h" -#include "Component.h" #include "meta/Index.h" #include "meta/VersionList.h" #include "meta/Version.h" @@ -495,6 +494,31 @@ static bool getTrivialComponentChanges(const ComponentIndex & index, const Requi return succeeded; } +QString ComponentUpdateTask::findBestComponentVersion(const ComponentPtr component) +{ + auto & components = d->m_list->d->components; + auto versions = component->getVersionList(); + versions->load(d->netmode); + + for (auto & version : versions->versions()) { + if (version->isRecommended()) { // only look at recommended versions + bool requirementsMet = true; + for (auto req : version->requires()) { + auto requirementMet = std::any_of(components.begin(), components.end(), [&req](ComponentPtr & cmp){ + return cmp->getID() == req.uid && cmp->getVersion() == req.equalsVersion; + }); + if (!requirementMet) { + requirementsMet = false; + } + } + + if (requirementsMet) // return first recommended version that meets all requirements + return version->version(); + } + } + return nullptr; +} + // FIXME, TODO: decouple dependency resolution from loading // FIXME: This works directly with the PackProfile internals. It shouldn't! It needs richer data types than PackProfile uses. // FIXME: throw all this away and use a graph @@ -591,27 +615,14 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) { component->m_version = "3.1.2"; } - else if (add.uid == "net.fabricmc.intermediary") - { - auto minecraft = std::find_if(components.begin(), components.end(), [](ComponentPtr & cmp){ - return cmp->getID() == "net.minecraft"; - }); - if(minecraft != components.end()) { - component->m_version = (*minecraft)->getVersion(); - } - } - else if (add.uid == "org.quiltmc.quilt-mappings") - { - auto minecraft = std::find_if(components.begin(), components.end(), [](ComponentPtr & cmp){ - return cmp->getID() == "net.minecraft"; - }); - if(minecraft != components.end()) { - component->m_version = (*minecraft)->getVersion() + "+build.1"; - } - } - } // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. // ############################################################################################################ +// below is not ugly anymore + else if (add.uid == "net.fabricmc.intermediary" || add.uid == "org.quiltmc.quilt-mappings") + { + component->m_version = findBestComponentVersion(component); + } + } } component->m_dependencyOnly = true; // FIXME: this should not work directly with the component list diff --git a/launcher/minecraft/ComponentUpdateTask.h b/launcher/minecraft/ComponentUpdateTask.h index 4274cabb..4fcbbca8 100644 --- a/launcher/minecraft/ComponentUpdateTask.h +++ b/launcher/minecraft/ComponentUpdateTask.h @@ -2,6 +2,7 @@ #include "tasks/Task.h" #include "net/Mode.h" +#include "Component.h" #include class PackProfile; @@ -26,6 +27,7 @@ protected: private: void loadComponents(); + QString findBestComponentVersion(ComponentPtr component); void resolveDependencies(bool checkOnly); void remoteLoadSucceeded(size_t index); From 74cdf5350de1649955814d6bcd596d8abfe9c5e2 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 20 Feb 2022 20:33:13 +0100 Subject: [PATCH 212/605] fix: restrict quilt-mappings versions to MC version --- launcher/ui/pages/instance/VersionPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index c8857017..6b9c82a2 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -395,7 +395,7 @@ void VersionPage::on_actionChange_version_triggered() return; } VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this); - if (uid == "net.fabricmc.intermediary") + if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.quilt-mappings") { vselect.setEmptyString(tr("No intermediary mappings versions are currently available.")); vselect.setEmptyErrorString(tr("Couldn't load or download the intermediary mappings version lists!")); From 35cfb41a9c8cf1328f3d2d14022cf51cbfc67f1f Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 20 Feb 2022 20:55:26 +0100 Subject: [PATCH 213/605] fix: check for Quilt as Fabric-compatible loader --- launcher/InstanceImportTask.cpp | 1 + launcher/minecraft/PackProfile.cpp | 17 +++++++++++++++++ launcher/minecraft/PackProfile.h | 3 +++ launcher/minecraft/mod/LocalModParseTask.cpp | 2 +- launcher/modplatform/ModAPI.h | 2 +- launcher/modplatform/flame/FlameModIndex.cpp | 1 + launcher/modplatform/modrinth/ModrinthAPI.h | 6 ++++-- launcher/ui/pages/instance/ModFolderPage.cpp | 8 +++++--- launcher/ui/pages/modplatform/ModModel.cpp | 15 ++++++--------- launcher/ui/pages/modplatform/ModModel.h | 1 - launcher/ui/pages/modplatform/ModPage.cpp | 16 +++++++++++++++- 11 files changed, 54 insertions(+), 18 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index a825e8d4..eeca29c6 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -241,6 +241,7 @@ void InstanceImportTask::processFlame() QString forgeVersion; QString fabricVersion; + // TODO: is Quilt relevant here? for(auto &loader: pack.minecraft.modLoaders) { auto id = loader.id; diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index d516e555..9889727e 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -970,3 +970,20 @@ void PackProfile::disableInteraction(bool disable) } } } + +ModAPI::ModLoaderType PackProfile::getModLoader() +{ + if (!getComponentVersion("net.minecraftforge").isEmpty()) + { + return ModAPI::Forge; + } + else if (!getComponentVersion("net.fabricmc.fabric-loader").isEmpty()) + { + return ModAPI::Fabric; + } + else if (!getComponentVersion("org.quiltmc.quilt-loader").isEmpty()) + { + return ModAPI::Quilt; + } + return ModAPI::Any; +} diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index 989d1c6a..ab4cd5c8 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -28,6 +28,7 @@ #include "BaseVersion.h" #include "MojangDownloadInfo.h" #include "net/Mode.h" +#include "modplatform/ModAPI.h" class MinecraftInstance; struct PackProfileData; @@ -117,6 +118,8 @@ public: // todo(merged): is this the best approach void appendComponent(ComponentPtr component); + ModAPI::ModLoaderType getModLoader(); + private: void scheduleSave(); bool saveIsScheduled() const; diff --git a/launcher/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/LocalModParseTask.cpp index 757a2187..f01da8ae 100644 --- a/launcher/minecraft/mod/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/LocalModParseTask.cpp @@ -391,7 +391,7 @@ void LocalModParseTask::processAsZip() zip.close(); return; } - else if (zip.setCurrentFile("fabric.mod.json")) + else if (zip.setCurrentFile("fabric.mod.json")) // TODO: Support quilt.mod.json { if (!file.open(QIODevice::ReadOnly)) { diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index ae6ac80f..1e38cf62 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -15,7 +15,7 @@ class ModAPI { virtual ~ModAPI() = default; // https://docs.curseforge.com/?http#tocS_ModLoaderType - enum ModLoaderType { Any = 0, Forge = 1, Cauldron = 2, LiteLoader = 3, Fabric = 4 }; + enum ModLoaderType { Any = 0, Forge = 1, Cauldron = 2, LiteLoader = 3, Fabric = 4, Quilt = 5 }; struct SearchArgs { int offset; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 2c3adee4..e86b64dd 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -69,6 +69,7 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, for (auto m : modules) { auto fname = Json::requireString(m.toObject(), "foldername"); // FIXME: This does not work properly when a mod supports more than one mod loader, since + // FIXME: This also doesn't deal with Quilt mods at the moment // they bundle the meta files for all of them in the same arquive, even when that version // doesn't support the given mod loader. if (hasFabric) { diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 30952e99..711649d9 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -55,11 +55,13 @@ class ModrinthAPI : public NetworkModAPI { { switch (modLoader) { case Any: - return "fabric, forge"; + return "fabric, forge, quilt"; case Forge: return "forge"; case Fabric: return "fabric"; + case Quilt: + return "quilt"; default: return ""; } @@ -67,7 +69,7 @@ class ModrinthAPI : public NetworkModAPI { inline auto validateModLoader(ModLoaderType modLoader) const -> bool { - return modLoader == Any || modLoader == Forge || modLoader == Fabric; + return modLoader == Any || modLoader == Forge || modLoader == Fabric || modLoader == Quilt; } }; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index fcb6022d..6acf94c7 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -56,6 +56,8 @@ #include "minecraft/VersionFilterData.h" #include "minecraft/PackProfile.h" +#include "modplatform/ModAPI.h" + #include "Version.h" #include "ui/dialogs/ProgressDialog.h" #include "tasks/SequentialTask.h" @@ -388,9 +390,9 @@ void ModFolderPage::on_actionInstall_mods_triggered() if(m_inst->typeName() != "Minecraft"){ return; //this is a null instance or a legacy instance } - bool hasFabric = !((MinecraftInstance *)m_inst)->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); - bool hasForge = !((MinecraftInstance *)m_inst)->getPackProfile()->getComponentVersion("net.minecraftforge").isEmpty(); - if (!hasFabric && !hasForge) { + QStringList modLoaders = {"net.minecraftforge", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader"}; + auto profile = ((MinecraftInstance *)m_inst)->getPackProfile(); + if (profile->getModLoader() == ModAPI::Any) { QMessageBox::critical(this,tr("Error"),tr("Please install a mod loader first!")); return; } diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 01b5d247..f75d2847 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -61,14 +61,18 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) { + auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); + m_parent->apiProvider()->getVersions(this, - { current.addonId.toString(), getMineVersions(), hasFabric() ? ModAPI::ModLoaderType::Fabric : ModAPI::ModLoaderType::Forge }); + { current.addonId.toString(), getMineVersions(), profile->getModLoader() }); } void ListModel::performPaginatedSearch() { + auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); + m_parent->apiProvider()->searchMods(this, - { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], hasFabric() ? ModAPI::Fabric : ModAPI::Forge, getMineVersions().at(0) }); + { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions().at(0) }); } void ListModel::searchWithTerm(const QString& term, const int sort) @@ -218,13 +222,6 @@ void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) } // namespace ModPlatform /******** Helpers ********/ -auto ModPlatform::ListModel::hasFabric() const -> bool -{ - return !(dynamic_cast((dynamic_cast(parent()))->m_instance)) - ->getPackProfile() - ->getComponentVersion("net.fabricmc.fabric-loader") - .isEmpty(); -} auto ModPlatform::ListModel::getMineVersions() const -> QList { diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 64cfa71e..dbadbeee 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -62,7 +62,6 @@ class ListModel : public QAbstractListModel { void requestLogo(QString file, QString url); - inline auto hasFabric() const -> bool; inline auto getMineVersions() const -> QList; protected: diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 3a116d3c..95e385cc 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -136,7 +136,21 @@ void ModPage::updateModVersions() auto packProfile = (dynamic_cast(m_instance))->getPackProfile(); QString mcVersion = packProfile->getComponentVersion("net.minecraft"); - QString loaderString = (packProfile->getComponentVersion("net.minecraftforge").isEmpty()) ? "fabric" : "forge"; + + QString loaderString; + switch (packProfile->getModLoader()) { + case ModAPI::Forge: + loaderString = "forge"; + break; + case ModAPI::Fabric: + loaderString = "fabric"; + break; + case ModAPI::Quilt: + loaderString = "quilt"; + break; + default: + break; + } for (int i = 0; i < current.versions.size(); i++) { auto version = current.versions[i]; From be2512bb4b05738011bfe165e890bcdd23ae8e92 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 7 Apr 2022 18:41:32 -0300 Subject: [PATCH 214/605] fix: issue with status of non-sequencial tasks --- launcher/tasks/Task.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 47c249b3..344a024e 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -53,7 +53,7 @@ class Task : public QObject { virtual bool canAbort() const { return false; } QString getStatus() { return m_status; } - virtual auto getStepStatus() const -> QString { return {}; } + virtual auto getStepStatus() const -> QString { return m_status; } qint64 getProgress() { return m_progress; } qint64 getTotalProgress() { return m_progressTotal; } From 167e32a69fc0b4224d10e7a3c7ce13e4cb4334c7 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 7 Apr 2022 18:56:34 -0300 Subject: [PATCH 215/605] fix: allow aborting CF modpack importing --- launcher/InstanceImportTask.cpp | 8 ++++++++ launcher/InstanceImportTask.h | 3 +++ 2 files changed, 11 insertions(+) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index a825e8d4..03e942f1 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -40,6 +40,14 @@ InstanceImportTask::InstanceImportTask(const QUrl sourceUrl) m_sourceUrl = sourceUrl; } +bool InstanceImportTask::abort() +{ + m_filesNetJob->abort(); + m_extractFuture.cancel(); + + return false; +} + void InstanceImportTask::executeTask() { if (m_sourceUrl.isLocalFile()) diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index a1990647..365c3dc4 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -37,6 +37,9 @@ class InstanceImportTask : public InstanceTask public: explicit InstanceImportTask(const QUrl sourceUrl); + bool canAbort() const override { return true; } + bool abort() override; + protected: //! Entry point for tasks. virtual void executeTask() override; From d0cda6d6051a09826820da3cd96fe5dc36b274f0 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 7 Apr 2022 19:03:29 -0300 Subject: [PATCH 216/605] test: add basic Task unit test Only only two tests for now. We can iterate on this later :^) This is to try to avoid breaking things again! --- launcher/CMakeLists.txt | 5 +++++ launcher/tasks/Task_test.cpp | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 launcher/tasks/Task_test.cpp diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 05af3503..42348792 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -413,6 +413,11 @@ set(TASKS_SOURCES tasks/SequentialTask.cpp ) +add_unit_test(Task + SOURCES tasks/Task_test.cpp + LIBS Launcher_logic + ) + set(SETTINGS_SOURCES # Settings settings/INIFile.cpp diff --git a/launcher/tasks/Task_test.cpp b/launcher/tasks/Task_test.cpp new file mode 100644 index 00000000..a9a28bd0 --- /dev/null +++ b/launcher/tasks/Task_test.cpp @@ -0,0 +1,42 @@ +#include +#include "TestUtil.h" + +#include "Task.h" + +/* Does nothing. Only used for testing. */ +class BasicTask : public Task { + Q_OBJECT + public: + explicit BasicTask() : Task() {}; + private: + void executeTask() override {}; +}; + +class TaskTest : public QObject { + Q_OBJECT + + private slots: + void test_SetStatus(){ + BasicTask t; + QString status {"test status"}; + + t.setStatus(status); + + QCOMPARE(t.getStatus(), status); + } + + void test_SetProgress(){ + BasicTask t; + int current = 42; + int total = 207; + + t.setProgress(current, total); + + QCOMPARE(t.getProgress(), current); + QCOMPARE(t.getTotalProgress(), total); + } +}; + +QTEST_GUILESS_MAIN(TaskTest) + +#include "Task_test.moc" From eeae3eca6719a43a3dd868c37e9f31b4463f4924 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 7 Apr 2022 19:42:26 -0300 Subject: [PATCH 217/605] test: add new test to Task test Also adds one more check to setStatus test --- launcher/tasks/Task_test.cpp | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/launcher/tasks/Task_test.cpp b/launcher/tasks/Task_test.cpp index a9a28bd0..9b6cc2e5 100644 --- a/launcher/tasks/Task_test.cpp +++ b/launcher/tasks/Task_test.cpp @@ -6,23 +6,49 @@ /* Does nothing. Only used for testing. */ class BasicTask : public Task { Q_OBJECT - public: - explicit BasicTask() : Task() {}; + + friend class TaskTest; + private: void executeTask() override {}; }; +/* Does nothing. Only used for testing. */ +class BasicTask_MultiStep : public Task { + Q_OBJECT + + friend class TaskTest; + + private: + auto isMultiStep() const -> bool override { return true; } + + void executeTask() override {}; +}; + class TaskTest : public QObject { Q_OBJECT private slots: - void test_SetStatus(){ + void test_SetStatus_NoMultiStep(){ BasicTask t; QString status {"test status"}; t.setStatus(status); QCOMPARE(t.getStatus(), status); + QCOMPARE(t.getStepStatus(), status); + } + + void test_SetStatus_MultiStep(){ + BasicTask_MultiStep t; + QString status {"test status"}; + + t.setStatus(status); + + QCOMPARE(t.getStatus(), status); + // Even though it is multi step, it does not override the getStepStatus method, + // so it should remain the same. + QCOMPARE(t.getStepStatus(), status); } void test_SetProgress(){ From 3024dbcf2c84f1fcc1d3753487352b25c467a041 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Fri, 8 Apr 2022 08:50:32 +0200 Subject: [PATCH 218/605] Apply suggestion Co-authored-by: Kenneth Chew <79120643+kthchew@users.noreply.github.com> --- launcher/JavaCommon.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index ce16e8de..e12f24aa 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -20,7 +20,7 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent) // block lunacy with passing required version to the JVM if (jvmargs.contains(QRegExp("-version:.*"))) { auto warnStr = QObject::tr( - "You tried to pass required java version argument to the JVM (using \"-version=xxx\"). This is not safe and will not be allowed.\n" + "You tried to pass required java version argument to the JVM (using \"-version:xxx\"). This is not safe and will not be allowed.\n" "This message will be displayed until you remove this from the JVM arguments."); CustomMessageBox::selectable( parent, QObject::tr("JVM arguments warning"), From 66caac0bbc3e213f9149d611e927b8a77fbfc38a Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Fri, 8 Apr 2022 11:16:00 +0200 Subject: [PATCH 219/605] Update launcher/JavaCommon.cpp Co-authored-by: Sefa Eyeoglu --- launcher/JavaCommon.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index e12f24aa..17278d86 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -20,7 +20,7 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent) // block lunacy with passing required version to the JVM if (jvmargs.contains(QRegExp("-version:.*"))) { auto warnStr = QObject::tr( - "You tried to pass required java version argument to the JVM (using \"-version:xxx\"). This is not safe and will not be allowed.\n" + "You tried to pass required Java version argument to the JVM (using \"-version:xxx\"). This is not safe and will not be allowed.\n" "This message will be displayed until you remove this from the JVM arguments."); CustomMessageBox::selectable( parent, QObject::tr("JVM arguments warning"), From 75fddd0052e67c52a9bfe1041be0709aa60b1a14 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 8 Apr 2022 15:37:18 -0400 Subject: [PATCH 220/605] Create menubar prototype Some stuff still needs to be fixed: - The close window option always closes the main window, even if it is not the currently active window (only applicable on systems with native menu bar) - None of the (text) editing actions are enabled - Actions related to instances should only be active when an instance is selected - The open wiki option ("PolyMC Help") needs to be implemented - Delete instance keyboard shortcut does not seem to work on my system. Test further - It would be nice if the profiles menu had all of the logged in accounts, and if they could be selected from that menu (preferably with keyboard shortcuts, probably Ctrl + 1, Ctrl + 2, ...) --- launcher/ui/MainWindow.cpp | 249 +++++++++++++++++++++++++++++++++++++ launcher/ui/MainWindow.h | 2 + 2 files changed, 251 insertions(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 47c469e9..45c19ca4 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -243,6 +244,42 @@ public: QHBoxLayout *horizontalLayout = nullptr; QStatusBar *statusBar = nullptr; + QMenuBar *menuBar = nullptr; + QMenu *fileMenu; + QMenu *editMenu; + QMenu *profileMenu; + QAction *newAct; + QAction *openAct; + QAction *openOfflineAct; + QAction *editInstanceAct; + QAction *editNotesAct; + QAction *editModsAct; + QAction *editWorldsAct; + QAction *manageScreenshotsAct; + QAction *changeGroupAct; + QAction *openMCFolderAct; + QAction *openConfigFolderAct; + QAction *openInstanceFolderAct; + QAction *exportInstanceAct; + QAction *deleteInstanceAct; + QAction *duplicateInstanceAct; + QAction *closeAct; + QAction *undoAct; + QAction *redoAct; + QAction *cutAct; + QAction *copyAct; + QAction *pasteAct; + QAction *selectAllAct; + QAction *manageAccountAct; + QAction *aboutAct; + QAction *settingsAct; + QAction *wikiAct; + QAction *newsAct; + QAction *reportBugAct; + QAction *matrixAct; + QAction *discordAct; + QAction *redditAct; + TranslatedToolbar mainToolBar; TranslatedToolbar instanceToolBar; TranslatedToolbar newsToolBar; @@ -431,6 +468,210 @@ public: MainWindow->addToolBar(Qt::TopToolBarArea, mainToolBar); } + void createMenuBar(MainWindow *MainWindow) + { + menuBar = new QMenuBar(MainWindow); + // There's already a toolbar, so hide this menu bar by default unless 'alt' is pressed on systems without native menu bar + menuBar->setVisible(false); + createMenuActions(MainWindow); + + // TODO: only enable options while an instance is selected (if applicable) + fileMenu = menuBar->addMenu(tr("&File")); + fileMenu->addAction(newAct); + fileMenu->addAction(openAct); + fileMenu->addAction(openOfflineAct); + fileMenu->addAction(closeAct); + fileMenu->addSeparator(); + fileMenu->addAction(editInstanceAct); + fileMenu->addAction(editNotesAct); + fileMenu->addAction(editModsAct); + fileMenu->addAction(editWorldsAct); + fileMenu->addAction(manageScreenshotsAct); + fileMenu->addAction(changeGroupAct); + fileMenu->addSeparator(); + fileMenu->addAction(openMCFolderAct); + fileMenu->addAction(openConfigFolderAct); + fileMenu->addAction(openInstanceFolderAct); + fileMenu->addSeparator(); + fileMenu->addAction(exportInstanceAct); + fileMenu->addAction(deleteInstanceAct); + fileMenu->addAction(duplicateInstanceAct); + fileMenu->addSeparator(); + + // TODO: functionality for edit actions. They're intended to be used where you can type text, e.g. notes. + editMenu = menuBar->addMenu(tr("&Edit")); + editMenu->addAction(undoAct); + editMenu->addAction(redoAct); + editMenu->addSeparator(); + editMenu->addAction(cutAct); + editMenu->addAction(copyAct); + editMenu->addAction(pasteAct); + editMenu->addAction(selectAllAct); + editMenu->addSeparator(); + + profileMenu = menuBar->addMenu(tr("&Profiles")); + // TODO: add a list of logged in accounts here + profileMenu->addAction(manageAccountAct); + + helpMenu = menuBar->addMenu(tr("&Help")); + helpMenu->addAction(aboutAct); + helpMenu->addAction(settingsAct); + helpMenu->addAction(wikiAct); + helpMenu->addAction(newsAct); + helpMenu->addSeparator(); + helpMenu->addAction(reportBugAct); + helpMenu->addAction(matrixAct); + helpMenu->addAction(discordAct); + helpMenu->addAction(redditAct); + + MainWindow->setMenuBar(menuBar); + } + + void createMenuActions(MainWindow *MainWindow) + { + newAct = new QAction(tr("&New Instance..."), MainWindow); + newAct->setShortcuts(QKeySequence::New); + newAct->setStatusTip(tr("Create a new instance")); + connect(newAct, &QAction::triggered, MainWindow, &MainWindow::on_actionAddInstance_triggered); + + openAct = new QAction(tr("&Launch"), MainWindow); + openAct->setShortcuts(QKeySequence::Open); + openAct->setStatusTip(tr("Launch the selected instance")); + connect(openAct, &QAction::triggered, MainWindow, &MainWindow::on_actionLaunchInstance_triggered); + + openOfflineAct = new QAction(tr("&Launch Offline"), MainWindow); + openOfflineAct->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); + openOfflineAct->setStatusTip(tr("Launch the selected instance in offline mode")); + connect(openOfflineAct, &QAction::triggered, MainWindow, &MainWindow::on_actionLaunchInstanceOffline_triggered); + + editInstanceAct = new QAction(tr("&Edit Instance..."), MainWindow); + editInstanceAct->setShortcut(QKeySequence(tr("Ctrl+I"))); + editInstanceAct->setStatusTip(tr("Edit the selected instance")); + connect(editInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionEditInstance_triggered); + + editNotesAct = new QAction(tr("&Edit Notes..."), MainWindow); + editNotesAct->setStatusTip(tr("Edit the selected instance's notes")); + connect(editNotesAct, &QAction::triggered, MainWindow, &MainWindow::on_actionEditInstNotes_triggered); + + editModsAct = new QAction(tr("&View Mods"), MainWindow); + editModsAct->setStatusTip(tr("View the selected instance's mods")); + connect(editModsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionMods_triggered); + + editWorldsAct = new QAction(tr("&View Worlds"), MainWindow); + editWorldsAct->setStatusTip(tr("View the selected instance's worlds")); + connect(editWorldsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionWorlds_triggered); + + manageScreenshotsAct = new QAction(tr("&Manage Screenshots"), MainWindow); + manageScreenshotsAct->setStatusTip(tr("Manage the selected instance's screenshots")); + connect(manageScreenshotsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionScreenshots_triggered); + + changeGroupAct = new QAction(tr("&Change Group..."), MainWindow); + changeGroupAct->setShortcut(QKeySequence(tr("Ctrl+G"))); + changeGroupAct->setStatusTip(tr("Change the selected instance's group")); + connect(changeGroupAct, &QAction::triggered, MainWindow, &MainWindow::on_actionChangeInstGroup_triggered); + + openMCFolderAct = new QAction(tr("&Open Minecraft Folder"), MainWindow); + openMCFolderAct->setShortcut(QKeySequence(tr("Ctrl+M"))); + openMCFolderAct->setStatusTip(tr("Open the selected instance's Minecraft folder")); + connect(openMCFolderAct, &QAction::triggered, MainWindow, &MainWindow::on_actionViewSelectedMCFolder_triggered); + + openConfigFolderAct = new QAction(tr("&Open Config Folder"), MainWindow); + openConfigFolderAct->setStatusTip(tr("Open the selected instance's config folder")); + connect(openConfigFolderAct, &QAction::triggered, MainWindow, &MainWindow::on_actionConfig_Folder_triggered); + + openInstanceFolderAct = new QAction(tr("&Open Instance Folder"), MainWindow); + openInstanceFolderAct->setStatusTip(tr("Open the selected instance's main folder")); + connect(openInstanceFolderAct, &QAction::triggered, MainWindow, &MainWindow::on_actionViewInstanceFolder_triggered); + + exportInstanceAct = new QAction(tr("&Export Instance..."), MainWindow); + exportInstanceAct->setShortcut(QKeySequence(tr("Ctrl+E"))); + exportInstanceAct->setStatusTip(tr("Export the selected instance")); + connect(exportInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionExportInstance_triggered); + + deleteInstanceAct = new QAction(tr("&Delete Instance..."), MainWindow); + deleteInstanceAct->setShortcut(QKeySequence::Delete); + deleteInstanceAct->setStatusTip(tr("Delete the selected instance")); + connect(deleteInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionDeleteInstance_triggered); + + duplicateInstanceAct = new QAction(tr("&Copy Instance..."), MainWindow); + duplicateInstanceAct->setShortcut(QKeySequence(tr("Ctrl+D"))); + duplicateInstanceAct->setStatusTip(tr("Duplicate the selected instance")); + connect(duplicateInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionCopyInstance_triggered); + + closeAct = new QAction(tr("&Close Window"), MainWindow); + closeAct->setShortcut(QKeySequence::Close); + closeAct->setStatusTip(tr("Close the current window")); + // FIXME: currently this always closes the main window, even if it is not currently the window in focus + connect(closeAct, &QAction::triggered, MainWindow, &MainWindow::close); + + undoAct = new QAction(tr("&Undo"), MainWindow); + undoAct->setShortcuts(QKeySequence::Undo); + undoAct->setStatusTip(tr("Undo")); + undoAct->setEnabled(false); + + redoAct = new QAction(tr("&Redo"), MainWindow); + redoAct->setShortcuts(QKeySequence::Redo); + redoAct->setStatusTip(tr("Redo")); + redoAct->setEnabled(false); + + cutAct = new QAction(tr("&Cut"), MainWindow); + cutAct->setShortcuts(QKeySequence::Cut); + cutAct->setStatusTip(tr("Cut")); + cutAct->setEnabled(false); + + copyAct = new QAction(tr("&Copy"), MainWindow); + copyAct->setShortcuts(QKeySequence::Copy); + copyAct->setStatusTip(tr("Copy")); + copyAct->setEnabled(false); + + pasteAct = new QAction(tr("&Paste"), MainWindow); + pasteAct->setShortcuts(QKeySequence::Paste); + pasteAct->setStatusTip(tr("Paste")); + pasteAct->setEnabled(false); + + selectAllAct = new QAction(tr("&Select All"), MainWindow); + selectAllAct->setShortcuts(QKeySequence::SelectAll); + selectAllAct->setStatusTip(tr("Select all")); + selectAllAct->setEnabled(false); + + manageAccountAct = new QAction(tr("&Manage Accounts..."), MainWindow); + manageAccountAct->setStatusTip(tr("Open account manager")); + connect(manageAccountAct, &QAction::triggered, MainWindow, &MainWindow::on_actionManageAccounts_triggered); + + aboutAct = new QAction(tr("&About"), MainWindow); + aboutAct->setStatusTip(tr("About %1").arg(BuildConfig.LAUNCHER_NAME)); + connect(aboutAct, &QAction::triggered, MainWindow, &MainWindow::on_actionAbout_triggered); + + settingsAct = new QAction(tr("&Settings..."), MainWindow); + settingsAct->setShortcut(QKeySequence::Preferences); + settingsAct->setStatusTip(tr("Change %1 settings").arg(BuildConfig.LAUNCHER_NAME)); + connect(settingsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionSettings_triggered); + + wikiAct = new QAction(tr("&%1 Help").arg(BuildConfig.LAUNCHER_NAME), MainWindow); + wikiAct->setStatusTip(tr("Open %1's wiki").arg(BuildConfig.LAUNCHER_NAME)); + connect(wikiAct, &QAction::triggered, MainWindow, &MainWindow::on_actionOpenWiki_triggered); + + newsAct = new QAction(tr("&%1 News").arg(BuildConfig.LAUNCHER_NAME), MainWindow); + newsAct->setStatusTip(tr("Open %1's news").arg(BuildConfig.LAUNCHER_NAME)); + connect(newsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionMoreNews_triggered); + + reportBugAct = new QAction(tr("&Report Bugs..."), MainWindow); + reportBugAct->setStatusTip(tr("Report bugs to the developers")); + connect(reportBugAct, &QAction::triggered, MainWindow, &MainWindow::on_actionReportBug_triggered); + + matrixAct = new QAction(tr("&Matrix"), MainWindow); + matrixAct->setStatusTip(tr("Open %1's Matrix space").arg(BuildConfig.LAUNCHER_NAME)); + connect(matrixAct, &QAction::triggered, MainWindow, &MainWindow::on_actionMATRIX_triggered); + + discordAct = new QAction(tr("&Discord"), MainWindow); + discordAct->setStatusTip(tr("Open %1's Discord guild").arg(BuildConfig.LAUNCHER_NAME)); + connect(discordAct, &QAction::triggered, MainWindow, &MainWindow::on_actionDISCORD_triggered); + + redditAct = new QAction(tr("&Reddit"), MainWindow); + redditAct->setStatusTip(tr("Open %1's subreddit").arg(BuildConfig.LAUNCHER_NAME)); + connect(redditAct, &QAction::triggered, MainWindow, &MainWindow::on_actionREDDIT_triggered); + } + void createStatusBar(QMainWindow *MainWindow) { statusBar = new QStatusBar(MainWindow); @@ -636,6 +877,8 @@ public: createMainToolbar(MainWindow); + createMenuBar(dynamic_cast(MainWindow)); + centralWidget = new QWidget(MainWindow); centralWidget->setObjectName(QStringLiteral("centralWidget")); horizontalLayout = new QHBoxLayout(centralWidget); @@ -1671,6 +1914,12 @@ void MainWindow::on_actionReportBug_triggered() DesktopServices::openUrl(QUrl(BuildConfig.BUG_TRACKER_URL)); } +void MainWindow::on_actionOpenWiki_triggered() +{ + // TODO: add functionality +// DesktopServices::openUrl(QUrl(BuildConfig.WIKI_URL)); +} + void MainWindow::on_actionMoreNews_triggered() { DesktopServices::openUrl(QUrl(BuildConfig.NEWS_OPEN_URL)); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index f2852d78..bd1a596c 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -110,6 +110,8 @@ private slots: void on_actionReportBug_triggered(); + void on_actionOpenWiki_triggered(); + void on_actionMoreNews_triggered(); void newsButtonClicked(); From ab82358dcb327a1a8ee19e102ce79c260e4f1241 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 8 Apr 2022 16:21:52 -0400 Subject: [PATCH 221/605] Show and hide the menu bar with the 'alt' key Only applicable for systems without a native menu bar (i.e. almost anything that is not macOS or Ubuntu Unity). On these systems, the menu bar appears on top of the window, which does not look good next to the tool bar already up there. When the menu bar is hidden, the keyboard shortcuts set by the menu bar are disabled. They should always work, so this also adds a workaround for that. --- launcher/ui/MainWindow.cpp | 59 ++++++++++++++++++++++++++++++++++++++ launcher/ui/MainWindow.h | 4 +++ 2 files changed, 63 insertions(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 45c19ca4..b98cd8b7 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -527,6 +527,7 @@ public: MainWindow->setMenuBar(menuBar); } + // If a keyboard shortcut is changed here, it must be changed below in keyPressEvent as well void createMenuActions(MainWindow *MainWindow) { newAct = new QAction(tr("&New Instance..."), MainWindow); @@ -1096,6 +1097,64 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow retranslateUi(); } +// macOS always has a native menu bar, so these fixes are not applicable +// Other systems may or may not have a native menu bar (most do not - it seems like only Ubuntu Unity does) +#ifdef Q_OS_MAC +void MainWindow::keyReleaseEvent(QKeyEvent *event) +{ + if(event->key()==Qt::Key_Alt) + ui->menuBar->setVisible(!ui->menuBar->isVisible()); +} + +// FIXME: This is a hack because keyboard shortcuts do nothing while menu bar is hidden on systems without native menu bar +// If a keyboard shortcut is changed above in `createMenuActions`, it must be changed here as well +void MainWindow::keyPressEvent(QKeyEvent *event) +{ + if(ui->menuBar->isVisible() || ui->menuBar->isNativeMenuBar()) + return; // let the menu bar handle the keyboard shortcuts + + if(event->modifiers().testFlag(Qt::ControlModifier)) + { + switch(event->key()) + { + case Qt::Key_N: + on_actionAddInstance_triggered(); + return; + case Qt::Key_O: + if(event->modifiers().testFlag(Qt::ShiftModifier)) + on_actionLaunchInstanceOffline_triggered(); + else + on_actionLaunchInstance_triggered(); + return; + case Qt::Key_I: + on_actionEditInstance_triggered(); + return; + case Qt::Key_G: + on_actionChangeInstGroup_triggered(); + return; + case Qt::Key_M: + on_actionViewSelectedMCFolder_triggered(); + return; + case Qt::Key_E: + on_actionExportInstance_triggered(); + return; + case Qt::Key_Delete: + on_actionDeleteInstance_triggered(); + return; + case Qt::Key_D: + on_actionCopyInstance_triggered(); + return; + case Qt::Key_W: + close(); + return; + // Text editing shortcuts are handled by the OS, so they do not need to be implemented here again + default: + return; + } + } +} +#endif + void MainWindow::retranslateUi() { auto accounts = APPLICATION->accounts(); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index bd1a596c..4ce4b7db 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -186,6 +186,10 @@ private slots: void globalSettingsClosed(); + void keyReleaseEvent(QKeyEvent *event) override; + + void keyPressEvent(QKeyEvent *event) override; + private: void retranslateUi(); From 9f3eed6ca25fdf773dfd38a7a86fba0ee7d67f5c Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 8 Apr 2022 17:00:42 -0400 Subject: [PATCH 222/605] Fix typos causing build failures on non-macOS systems It also did the exact opposite thing I was trying to do, so that's fixed too... --- launcher/ui/MainWindow.cpp | 7 ++++++- launcher/ui/MainWindow.h | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index b98cd8b7..895b9881 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1099,11 +1099,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow // macOS always has a native menu bar, so these fixes are not applicable // Other systems may or may not have a native menu bar (most do not - it seems like only Ubuntu Unity does) -#ifdef Q_OS_MAC +#ifndef Q_OS_MAC void MainWindow::keyReleaseEvent(QKeyEvent *event) { if(event->key()==Qt::Key_Alt) ui->menuBar->setVisible(!ui->menuBar->isVisible()); + else + QMainWindow::keyReleaseEvent(event); } // FIXME: This is a hack because keyboard shortcuts do nothing while menu bar is hidden on systems without native menu bar @@ -1111,7 +1113,10 @@ void MainWindow::keyReleaseEvent(QKeyEvent *event) void MainWindow::keyPressEvent(QKeyEvent *event) { if(ui->menuBar->isVisible() || ui->menuBar->isNativeMenuBar()) + { + QMainWindow::keyPressEvent(event); return; // let the menu bar handle the keyboard shortcuts + } if(event->modifiers().testFlag(Qt::ControlModifier)) { diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 4ce4b7db..c38ee073 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -186,9 +186,11 @@ private slots: void globalSettingsClosed(); +#ifndef Q_OS_MAC void keyReleaseEvent(QKeyEvent *event) override; void keyPressEvent(QKeyEvent *event) override; +#endif private: void retranslateUi(); From 89125fde22d39aed93ab516956b3a4e30cca7a88 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 9 Apr 2022 14:56:07 +0200 Subject: [PATCH 223/605] refactor: switch Quilt mappings to hashed MojMap --- launcher/minecraft/ComponentUpdateTask.cpp | 39 +++++----------------- launcher/ui/pages/instance/VersionPage.cpp | 2 +- 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index a0559232..0095c613 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -494,31 +494,6 @@ static bool getTrivialComponentChanges(const ComponentIndex & index, const Requi return succeeded; } -QString ComponentUpdateTask::findBestComponentVersion(const ComponentPtr component) -{ - auto & components = d->m_list->d->components; - auto versions = component->getVersionList(); - versions->load(d->netmode); - - for (auto & version : versions->versions()) { - if (version->isRecommended()) { // only look at recommended versions - bool requirementsMet = true; - for (auto req : version->requires()) { - auto requirementMet = std::any_of(components.begin(), components.end(), [&req](ComponentPtr & cmp){ - return cmp->getID() == req.uid && cmp->getVersion() == req.equalsVersion; - }); - if (!requirementMet) { - requirementsMet = false; - } - } - - if (requirementsMet) // return first recommended version that meets all requirements - return version->version(); - } - } - return nullptr; -} - // FIXME, TODO: decouple dependency resolution from loading // FIXME: This works directly with the PackProfile internals. It shouldn't! It needs richer data types than PackProfile uses. // FIXME: throw all this away and use a graph @@ -615,13 +590,17 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) { component->m_version = "3.1.2"; } + else if (add.uid == "net.fabricmc.intermediary" || add.uid == "org.quiltmc.hashed") + { + auto minecraft = std::find_if(components.begin(), components.end(), [](ComponentPtr & cmp){ + return cmp->getID() == "net.minecraft"; + }); + if(minecraft != components.end()) { + component->m_version = (*minecraft)->getVersion(); + } + } // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. // ############################################################################################################ -// below is not ugly anymore - else if (add.uid == "net.fabricmc.intermediary" || add.uid == "org.quiltmc.quilt-mappings") - { - component->m_version = findBestComponentVersion(component); - } } } component->m_dependencyOnly = true; diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 6b9c82a2..7264f362 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -395,7 +395,7 @@ void VersionPage::on_actionChange_version_triggered() return; } VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this); - if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.quilt-mappings") + if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.hashed") { vselect.setEmptyString(tr("No intermediary mappings versions are currently available.")); vselect.setEmptyErrorString(tr("Couldn't load or download the intermediary mappings version lists!")); From 54e4f88ada41d3cdb0baf7a4bb4ea89315481d9d Mon Sep 17 00:00:00 2001 From: Regular Baf <95348524+Regular-Baf@users.noreply.github.com> Date: Sat, 9 Apr 2022 13:00:27 +0000 Subject: [PATCH 224/605] Attempt implementing the new discord logo --- .../resources/multimc/scalable/discord.svg | 116 ++---------------- 1 file changed, 9 insertions(+), 107 deletions(-) diff --git a/launcher/resources/multimc/scalable/discord.svg b/launcher/resources/multimc/scalable/discord.svg index 067be1e8..3efe1ec1 100644 --- a/launcher/resources/multimc/scalable/discord.svg +++ b/launcher/resources/multimc/scalable/discord.svg @@ -1,108 +1,10 @@ - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - + + + + + + + + + From fcb311eecd16ebd48dab37aca5eecdaded9bb70f Mon Sep 17 00:00:00 2001 From: Regular Baf <95348524+Regular-Baf@users.noreply.github.com> Date: Sat, 9 Apr 2022 13:11:33 +0000 Subject: [PATCH 225/605] size doesnt matter? --- launcher/resources/multimc/scalable/discord.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/resources/multimc/scalable/discord.svg b/launcher/resources/multimc/scalable/discord.svg index 3efe1ec1..def496fb 100644 --- a/launcher/resources/multimc/scalable/discord.svg +++ b/launcher/resources/multimc/scalable/discord.svg @@ -1,4 +1,4 @@ - + From abfb99ba3fc113c11c3b2c2c66634e44c43f4167 Mon Sep 17 00:00:00 2001 From: Regular Baf <95348524+Regular-Baf@users.noreply.github.com> Date: Sat, 9 Apr 2022 13:16:03 +0000 Subject: [PATCH 226/605] Nevermind. It does. --- launcher/resources/multimc/scalable/discord.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/resources/multimc/scalable/discord.svg b/launcher/resources/multimc/scalable/discord.svg index def496fb..a7eae7ed 100644 --- a/launcher/resources/multimc/scalable/discord.svg +++ b/launcher/resources/multimc/scalable/discord.svg @@ -1,4 +1,4 @@ - + From 99193a2d7b5a1e68e269465561cf8f476b6b0c41 Mon Sep 17 00:00:00 2001 From: Regular Baf <95348524+Regular-Baf@users.noreply.github.com> Date: Sat, 9 Apr 2022 13:16:55 +0000 Subject: [PATCH 227/605] Update discord.svg --- launcher/resources/multimc/scalable/discord.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/resources/multimc/scalable/discord.svg b/launcher/resources/multimc/scalable/discord.svg index a7eae7ed..19600c6e 100644 --- a/launcher/resources/multimc/scalable/discord.svg +++ b/launcher/resources/multimc/scalable/discord.svg @@ -4,7 +4,7 @@ - + From 37a30fbc3fe299f975c4492fd9d397d9cc71bd4a Mon Sep 17 00:00:00 2001 From: Regular Baf <95348524+Regular-Baf@users.noreply.github.com> Date: Sat, 9 Apr 2022 13:22:39 +0000 Subject: [PATCH 228/605] Update discord.svg --- launcher/resources/multimc/scalable/discord.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/resources/multimc/scalable/discord.svg b/launcher/resources/multimc/scalable/discord.svg index 19600c6e..3efe1ec1 100644 --- a/launcher/resources/multimc/scalable/discord.svg +++ b/launcher/resources/multimc/scalable/discord.svg @@ -1,10 +1,10 @@ - + - + From ea3ceb382a08e58513bd06af99021796f746e65b Mon Sep 17 00:00:00 2001 From: Regular Baf <95348524+Regular-Baf@users.noreply.github.com> Date: Sat, 9 Apr 2022 23:42:08 +0000 Subject: [PATCH 229/605] Update launcher/resources/multimc/scalable/discord.svg Co-authored-by: Sefa Eyeoglu --- .../resources/multimc/scalable/discord.svg | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/launcher/resources/multimc/scalable/discord.svg b/launcher/resources/multimc/scalable/discord.svg index 3efe1ec1..e37c3b84 100644 --- a/launcher/resources/multimc/scalable/discord.svg +++ b/launcher/resources/multimc/scalable/discord.svg @@ -1,10 +1,11 @@ - - - - - - - - - + + + + + + + + + + From fa2b3bcc633ce6f658d659fb3c0a87ddb6bc91ba Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 10 Apr 2022 22:09:59 +0200 Subject: [PATCH 230/605] feat: install manpage --- CMakeLists.txt | 2 ++ program_info/CMakeLists.txt | 1 + doc/polymc.1.txt => program_info/polymc.6.txt | 4 +--- 3 files changed, 4 insertions(+), 3 deletions(-) rename doc/polymc.1.txt => program_info/polymc.6.txt (95%) diff --git a/CMakeLists.txt b/CMakeLists.txt index d45d4975..1c51d7d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -204,6 +204,7 @@ elseif(UNIX) set(LAUNCHER_DESKTOP_DEST_DIR "share/applications" CACHE STRING "Path to the desktop file directory") set(LAUNCHER_METAINFO_DEST_DIR "share/metainfo" CACHE STRING "Path to the metainfo directory") set(LAUNCHER_ICON_DEST_DIR "share/icons/hicolor/scalable/apps" CACHE STRING "Path to the scalable icon directory") + set(LAUNCHER_MAN_DEST_DIR "share/man/man6" CACHE STRING "Path to the man page directory") # jars path is determined on runtime, relative to "Application root path", generally /usr for Launcher_PORTABLE=0 set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_JARS_LOCATION=${JARS_DEST_DIR}") @@ -211,6 +212,7 @@ elseif(UNIX) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${LAUNCHER_DESKTOP_DEST_DIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${LAUNCHER_METAINFO_DEST_DIR}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION ${LAUNCHER_ICON_DEST_DIR}) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_ManPage} DESTINATION ${LAUNCHER_MAN_DEST_DIR} RENAME "${Launcher_APP_BINARY_NAME}.6") endif() # install as bundle with no dependencies included diff --git a/program_info/CMakeLists.txt b/program_info/CMakeLists.txt index f9d7621d..9c243826 100644 --- a/program_info/CMakeLists.txt +++ b/program_info/CMakeLists.txt @@ -11,6 +11,7 @@ set(Launcher_DesktopFileName "org.polymc.PolyMC.desktop" PARENT_SCOPE) set(Launcher_Desktop "program_info/org.polymc.PolyMC.desktop" PARENT_SCOPE) set(Launcher_MetaInfo "program_info/org.polymc.PolyMC.metainfo.xml" PARENT_SCOPE) +set(Launcher_ManPage "program_info/polymc.6.txt" PARENT_SCOPE) set(Launcher_SVG "program_info/org.polymc.PolyMC.svg" PARENT_SCOPE) set(Launcher_Branding_ICNS "program_info/polymc.icns" PARENT_SCOPE) set(Launcher_Branding_WindowsRC "program_info/polymc.rc" PARENT_SCOPE) diff --git a/doc/polymc.1.txt b/program_info/polymc.6.txt similarity index 95% rename from doc/polymc.1.txt rename to program_info/polymc.6.txt index 9ba34662..8f126cce 100644 --- a/doc/polymc.1.txt +++ b/program_info/polymc.6.txt @@ -59,8 +59,6 @@ Main website: AUTHORS ------- -peterix - -swurl +PolyMC Contributors // vim: syntax=asciidoc From 14a0e8586222dcb2fa4af9b89c49276aaea84347 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 14 Apr 2022 16:50:04 +0200 Subject: [PATCH 231/605] fix: remove unused code --- launcher/minecraft/ComponentUpdateTask.cpp | 3 ++- launcher/minecraft/ComponentUpdateTask.h | 2 -- launcher/ui/pages/instance/ModFolderPage.cpp | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index 0095c613..ff7ed0af 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -2,6 +2,7 @@ #include "PackProfile_p.h" #include "PackProfile.h" +#include "Component.h" #include "meta/Index.h" #include "meta/VersionList.h" #include "meta/Version.h" @@ -599,9 +600,9 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) component->m_version = (*minecraft)->getVersion(); } } + } // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. // ############################################################################################################ - } } component->m_dependencyOnly = true; // FIXME: this should not work directly with the component list diff --git a/launcher/minecraft/ComponentUpdateTask.h b/launcher/minecraft/ComponentUpdateTask.h index 4fcbbca8..4274cabb 100644 --- a/launcher/minecraft/ComponentUpdateTask.h +++ b/launcher/minecraft/ComponentUpdateTask.h @@ -2,7 +2,6 @@ #include "tasks/Task.h" #include "net/Mode.h" -#include "Component.h" #include class PackProfile; @@ -27,7 +26,6 @@ protected: private: void loadComponents(); - QString findBestComponentVersion(ComponentPtr component); void resolveDependencies(bool checkOnly); void remoteLoadSucceeded(size_t index); diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 6acf94c7..46235462 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -390,7 +390,6 @@ void ModFolderPage::on_actionInstall_mods_triggered() if(m_inst->typeName() != "Minecraft"){ return; //this is a null instance or a legacy instance } - QStringList modLoaders = {"net.minecraftforge", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader"}; auto profile = ((MinecraftInstance *)m_inst)->getPackProfile(); if (profile->getModLoader() == ModAPI::Any) { QMessageBox::critical(this,tr("Error"),tr("Please install a mod loader first!")); From 18ac109e5abb86eebd254931efeea3630371a0bb Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 14 Apr 2022 17:20:07 +0200 Subject: [PATCH 232/605] fix: support Quilt from Minecraft 1.14 onwards --- launcher/ui/pages/instance/VersionPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 7264f362..23e2367b 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -243,7 +243,7 @@ void VersionPage::updateVersionControls() bool supportsFabric = minecraftVersion >= Version("1.14"); ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric); - bool supportsQuilt = minecraftVersion >= Version("1.17.1"); + bool supportsQuilt = minecraftVersion >= Version("1.14"); ui->actionInstall_Quilt->setEnabled(controlsEnabled && supportsQuilt); bool supportsLiteLoader = minecraftVersion <= Version("1.12.2"); From 9fb5674233c21775fac76cf96cd2a77c4098e908 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 14 Apr 2022 21:55:03 +0200 Subject: [PATCH 233/605] refactor: cleanup ModLoaderType --- launcher/minecraft/PackProfile.cpp | 2 +- launcher/modplatform/ModAPI.h | 20 ++++++++++++++++- launcher/modplatform/modrinth/ModrinthAPI.h | 19 +++++----------- launcher/ui/pages/instance/ModFolderPage.cpp | 2 +- launcher/ui/pages/modplatform/ModPage.cpp | 23 +++++--------------- 5 files changed, 31 insertions(+), 35 deletions(-) diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 9889727e..d53f41e1 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -985,5 +985,5 @@ ModAPI::ModLoaderType PackProfile::getModLoader() { return ModAPI::Quilt; } - return ModAPI::Any; + return ModAPI::Unspecified; } diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index 1e38cf62..1a562172 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -15,7 +15,7 @@ class ModAPI { virtual ~ModAPI() = default; // https://docs.curseforge.com/?http#tocS_ModLoaderType - enum ModLoaderType { Any = 0, Forge = 1, Cauldron = 2, LiteLoader = 3, Fabric = 4, Quilt = 5 }; + enum ModLoaderType { Unspecified = 0, Forge = 1, Cauldron = 2, LiteLoader = 3, Fabric = 4, Quilt = 5 }; struct SearchArgs { int offset; @@ -35,4 +35,22 @@ class ModAPI { }; virtual void getVersions(CallerType* caller, VersionSearchArgs&& args) const = 0; + + static auto getModLoaderString(ModLoaderType type) -> const QString { + switch (type) { + case Unspecified: + break; + case Forge: + return "forge"; + case Cauldron: + return "cauldron"; + case LiteLoader: + return "liteloader"; + case Fabric: + return "fabric"; + case Quilt: + return "quilt"; + } + return ""; + } }; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 711649d9..eefa4a85 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -51,25 +51,16 @@ class ModrinthAPI : public NetworkModAPI { return s; } - inline auto getModLoaderString(ModLoaderType modLoader) const -> QString + static auto getModLoaderString(ModLoaderType type) -> const QString { - switch (modLoader) { - case Any: - return "fabric, forge, quilt"; - case Forge: - return "forge"; - case Fabric: - return "fabric"; - case Quilt: - return "quilt"; - default: - return ""; - } + if (type == Unspecified) + return "fabric, forge, quilt"; + return ModAPI::getModLoaderString(type); } inline auto validateModLoader(ModLoaderType modLoader) const -> bool { - return modLoader == Any || modLoader == Forge || modLoader == Fabric || modLoader == Quilt; + return modLoader == Unspecified || modLoader == Forge || modLoader == Fabric || modLoader == Quilt; } }; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 46235462..8113fe85 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -391,7 +391,7 @@ void ModFolderPage::on_actionInstall_mods_triggered() return; //this is a null instance or a legacy instance } auto profile = ((MinecraftInstance *)m_inst)->getPackProfile(); - if (profile->getModLoader() == ModAPI::Any) { + if (profile->getModLoader() == ModAPI::Unspecified) { QMessageBox::critical(this,tr("Error"),tr("Please install a mod loader first!")); return; } diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 95e385cc..eabd8379 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -68,7 +68,7 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) text = name; else text = "" + name + ""; - + if (!current.authors.empty()) { auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { if (author.url.isEmpty()) { return author.name; } @@ -128,7 +128,7 @@ void ModPage::onModSelected() void ModPage::retranslate() { - ui->retranslateUi(this); + ui->retranslateUi(this); } void ModPage::updateModVersions() @@ -137,26 +137,13 @@ void ModPage::updateModVersions() QString mcVersion = packProfile->getComponentVersion("net.minecraft"); - QString loaderString; - switch (packProfile->getModLoader()) { - case ModAPI::Forge: - loaderString = "forge"; - break; - case ModAPI::Fabric: - loaderString = "fabric"; - break; - case ModAPI::Quilt: - loaderString = "quilt"; - break; - default: - break; - } + QString loaderString = ModAPI::getModLoaderString(packProfile->getModLoader()); for (int i = 0; i < current.versions.size(); i++) { auto version = current.versions[i]; //NOTE: Flame doesn't care about loaderString, so passing it changes nothing. - if (!validateVersion(version, mcVersion, loaderString)) { - continue; + if (!validateVersion(version, mcVersion, loaderString)) { + continue; } ui->versionSelectionBox->addItem(version.version, QVariant(i)); } From abb20c65e33a756288c14f746cbd6cc12b1f14d1 Mon Sep 17 00:00:00 2001 From: Irgendwer01 <67506320+Irgendwer01@users.noreply.github.com> Date: Fri, 15 Apr 2022 01:40:25 +0200 Subject: [PATCH 234/605] better FreeBSD support --- launcher/minecraft/MinecraftInstance.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 3ba79178..e40d2534 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -213,8 +213,12 @@ QString MinecraftInstance::binRoot() const QString MinecraftInstance::getNativePath() const { +#if defined(Q_OS_FREEBSD) + QDir natives_dir("/usr/local/lib/lwjgl/"); +#else QDir natives_dir(FS::PathCombine(instanceRoot(), "natives/")); return natives_dir.absolutePath(); +#endif } QString MinecraftInstance::getLocalLibraryPath() const From 06d9821b2cf7ab42805853ac20b1b0371addffba Mon Sep 17 00:00:00 2001 From: Irgendwer01 <67506320+Irgendwer01@users.noreply.github.com> Date: Fri, 15 Apr 2022 01:51:28 +0200 Subject: [PATCH 235/605] Update MinecraftInstance.cpp --- launcher/minecraft/MinecraftInstance.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index e40d2534..3e495249 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -213,7 +213,7 @@ QString MinecraftInstance::binRoot() const QString MinecraftInstance::getNativePath() const { -#if defined(Q_OS_FREEBSD) +#if defined(Os_FreeBSD) QDir natives_dir("/usr/local/lib/lwjgl/"); #else QDir natives_dir(FS::PathCombine(instanceRoot(), "natives/")); From 1dd663af6eb5f7d72baab7281a47f838077fb4cb Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Fri, 15 Apr 2022 11:07:42 +0200 Subject: [PATCH 236/605] CHANGE: switch the help pages to their own dir also renames modrinth-platform/curseforge-platform to just Mod-platform since they have the pages are basically the same --- CMakeLists.txt | 2 +- launcher/ui/pages/modplatform/flame/FlameModPage.h | 2 +- launcher/ui/pages/modplatform/modrinth/ModrinthPage.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d45d4975..e1d43ddc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,7 +48,7 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") ######## Set URLs ######## set(Launcher_NEWS_RSS_URL "https://polymc.org/feed/feed.xml" CACHE STRING "URL to fetch PolyMC's news RSS feed from.") set(Launcher_NEWS_OPEN_URL "https://polymc.org/news" CACHE STRING "URL that gets opened when the user clicks 'More News'") -set(Launcher_HELP_URL "https://polymc.org/wiki/%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://polymc.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 version numbers ######## set(Launcher_VERSION_MAJOR 1) diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index dc58fd7f..d96a0720 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -49,7 +49,7 @@ class FlameModPage : public ModPage { inline auto displayName() const -> QString override { return "CurseForge"; } inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("flame"); } inline auto id() const -> QString override { return "curseforge"; } - inline auto helpPage() const -> QString override { return "Flame-platform"; } + inline auto helpPage() const -> QString override { return "Mod-platform"; } inline auto debugName() const -> QString override { return "Flame"; } inline auto metaEntryBase() const -> QString override { return "FlameMods"; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index aa5ed793..0bde43eb 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -49,7 +49,7 @@ class ModrinthPage : public ModPage { inline auto displayName() const -> QString override { return "Modrinth"; } inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); } inline auto id() const -> QString override { return "modrinth"; } - inline auto helpPage() const -> QString override { return "Modrinth-platform"; } + inline auto helpPage() const -> QString override { return "Mod-platform"; } inline auto debugName() const -> QString override { return "Modrinth"; } inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; }; From 9a120f43c8df3062e34c32b28f3060b95e61249a Mon Sep 17 00:00:00 2001 From: Irgendwer01 <67506320+Irgendwer01@users.noreply.github.com> Date: Fri, 15 Apr 2022 13:03:48 +0200 Subject: [PATCH 237/605] Update MinecraftInstance.cpp --- launcher/minecraft/MinecraftInstance.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 3e495249..503b34d9 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -213,7 +213,7 @@ QString MinecraftInstance::binRoot() const QString MinecraftInstance::getNativePath() const { -#if defined(Os_FreeBSD) +#ifdef Q_OS_FREEBSD QDir natives_dir("/usr/local/lib/lwjgl/"); #else QDir natives_dir(FS::PathCombine(instanceRoot(), "natives/")); From e0ab8207ed3614bf078cc2d79f154afeb9d45737 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 2 Apr 2022 18:29:35 -0300 Subject: [PATCH 238/605] feat: Add dialog to filter mod options in mod download --- launcher/CMakeLists.txt | 3 + launcher/ui/dialogs/FilterModsDialog.cpp | 75 ++++++++++ launcher/ui/dialogs/FilterModsDialog.h | 54 +++++++ launcher/ui/dialogs/FilterModsDialog.ui | 181 +++++++++++++++++++++++ 4 files changed, 313 insertions(+) create mode 100644 launcher/ui/dialogs/FilterModsDialog.cpp create mode 100644 launcher/ui/dialogs/FilterModsDialog.h create mode 100644 launcher/ui/dialogs/FilterModsDialog.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 42348792..8579071a 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -824,6 +824,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/SkinUploadDialog.h ui/dialogs/ModDownloadDialog.cpp ui/dialogs/ModDownloadDialog.h + ui/dialogs/FilterModsDialog.cpp + ui/dialogs/FilterModsDialog.h # GUI - widgets @@ -923,6 +925,7 @@ qt5_wrap_ui(LAUNCHER_UI ui/dialogs/LoginDialog.ui ui/dialogs/EditAccountDialog.ui ui/dialogs/ReviewMessageBox.ui + ui/dialogs/FilterModsDialog.ui ) qt5_add_resources(LAUNCHER_RESOURCES diff --git a/launcher/ui/dialogs/FilterModsDialog.cpp b/launcher/ui/dialogs/FilterModsDialog.cpp new file mode 100644 index 00000000..a36f4ba4 --- /dev/null +++ b/launcher/ui/dialogs/FilterModsDialog.cpp @@ -0,0 +1,75 @@ +#include "FilterModsDialog.h" +#include "ui_FilterModsDialog.h" + +FilterModsDialog::FilterModsDialog(Version def, QWidget* parent) + : QDialog(parent), m_filter(new Filter()), ui(new Ui::FilterModsDialog) +{ + ui->setupUi(this); + + m_mcVersion_buttons.addButton(ui->strictVersionButton, VersionButtonID::Strict); + m_mcVersion_buttons.addButton(ui->majorVersionButton, VersionButtonID::Major); + m_mcVersion_buttons.addButton(ui->allVersionsButton, VersionButtonID::All); + m_mcVersion_buttons.addButton(ui->betweenVersionsButton, VersionButtonID::Between); + + connect(&m_mcVersion_buttons, SIGNAL(idClicked(int)), this, SLOT(onVersionFilterChanged(int))); + + m_filter->versions.push_front(def); +} + +int FilterModsDialog::execWithInstance(MinecraftInstance* instance) +{ + m_instance = instance; + + // Fix first openening behaviour + onVersionFilterChanged(0); + + auto mcVersionSplit = mcVersionStr().split("."); + + ui->strictVersionButton->setText( + tr("Strict match (= %1)").arg(mcVersionStr())); + ui->majorVersionButton->setText( + tr("Major varsion match (= %1.%2.x)").arg(mcVersionSplit[0], mcVersionSplit[1])); + ui->allVersionsButton->setText( + tr("Any version match")); + ui->betweenVersionsButton->setText( + tr("Between two versions")); + + int ret = QDialog::exec(); + m_instance = nullptr; + return ret; +} + +void FilterModsDialog::onVersionFilterChanged(int id) +{ + ui->lowerVersionComboBox->setEnabled(id == VersionButtonID::Between); + ui->upperVersionComboBox->setEnabled(id == VersionButtonID::Between); + + auto versionSplit = mcVersionStr().split("."); + int index = 0; + + m_filter->versions.clear(); + + switch(id){ + case(VersionButtonID::Strict): + m_filter->versions.push_front(mcVersion()); + break; + case(VersionButtonID::Major): + for(auto i = Version(QString("%1.%2").arg(versionSplit[0], versionSplit[1])); i <= mcVersion(); index++){ + m_filter->versions.push_front(i); + i = Version(QString("%1.%2.%3").arg(versionSplit[0], versionSplit[1], QString("%1").arg(index))); + } + break; + case(VersionButtonID::All): + break; + case(VersionButtonID::Between): + // TODO + break; + default: + break; + } +} + +FilterModsDialog::~FilterModsDialog() +{ + delete ui; +} diff --git a/launcher/ui/dialogs/FilterModsDialog.h b/launcher/ui/dialogs/FilterModsDialog.h new file mode 100644 index 00000000..b119e8e5 --- /dev/null +++ b/launcher/ui/dialogs/FilterModsDialog.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +#include "Version.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +class MinecraftInstance; + +namespace Ui { +class FilterModsDialog; +} + +class FilterModsDialog : public QDialog +{ + Q_OBJECT +public: + struct Filter { + std::list versions; + }; + + std::shared_ptr m_filter; + +public: + explicit FilterModsDialog(Version def, QWidget* parent = nullptr); + ~FilterModsDialog(); + + int execWithInstance(MinecraftInstance* instance); + + auto getFilter() -> std::shared_ptr { return m_filter; } + +private: + enum VersionButtonID { + Strict = 0, + Major = 1, + All = 2, + Between = 3 + }; + + inline auto mcVersionStr() const -> QString { return m_instance ? m_instance->getPackProfile()->getComponentVersion("net.minecraft") : ""; } + inline auto mcVersion() const -> Version { return { mcVersionStr() }; } + +private slots: + void onVersionFilterChanged(int id); + +private: + Ui::FilterModsDialog* ui; + + MinecraftInstance* m_instance = nullptr; + + QButtonGroup m_mcVersion_buttons; +}; diff --git a/launcher/ui/dialogs/FilterModsDialog.ui b/launcher/ui/dialogs/FilterModsDialog.ui new file mode 100644 index 00000000..da368aac --- /dev/null +++ b/launcher/ui/dialogs/FilterModsDialog.ui @@ -0,0 +1,181 @@ + + + FilterModsDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 345 + 323 + + + + Copy Instance + + + + :/icons/toolbar/copy:/icons/toolbar/copy + + + true + + + true + + + + + + + 0 + 0 + + + + Minecraft versions + + + false + + + false + + + + + + QLayout::SetDefaultConstraint + + + + + + + StrictVersion + + + true + + + + + + + MajorVersion + + + + + + + AllVersions + + + + + + + + + + + + + BetweenVersions + + + + + + + From + + + + + + + + + + To + + + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + FilterModsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + FilterModsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From c2b97c3e3f5ecb5a995b342c4e0afec6a1a3b2f6 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 2 Apr 2022 18:33:50 -0300 Subject: [PATCH 239/605] feat: Integrate newly created filter dialog in ModPage --- launcher/ui/dialogs/ModDownloadDialog.cpp | 2 +- launcher/ui/pages/modplatform/ModPage.cpp | 28 +++++++++++++++++++---- launcher/ui/pages/modplatform/ModPage.h | 7 ++++++ launcher/ui/pages/modplatform/ModPage.ui | 11 +++++++-- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index a53f93e8..9aac4145 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -80,7 +80,7 @@ void ModDownloadDialog::confirm() tr("Confirm mods to download") ); - for(auto task : keys){ + for(auto& task : keys){ confirm_dialog->appendMod(task, modTask.find(task).value()->getFilename()); } diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index eabd8379..16e44c0d 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -2,21 +2,29 @@ #include "ui_ModPage.h" #include +#include #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) - : QWidget(dialog), m_instance(instance), ui(new Ui::ModPage), dialog(dialog), api(api) + : QWidget(dialog) + , m_instance(instance) + , ui(new Ui::ModPage) + , dialog(dialog) + , filter_dialog(static_cast(instance)->getPackProfile()->getComponentVersion("net.minecraft"), this) + , api(api) { ui->setupUi(this); connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); + connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); ui->searchEdit->installEventFilter(this); ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + m_filter = filter_dialog.getFilter(); } ModPage::~ModPage() @@ -49,6 +57,13 @@ auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool /******** Callbacks to events in the UI (set up in the derived classes) ********/ +void ModPage::filterMods() +{ + filter_dialog.execWithInstance(static_cast(m_instance)); + + m_filter = filter_dialog.getFilter(); +} + void ModPage::triggerSearch() { listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); @@ -141,11 +156,16 @@ void ModPage::updateModVersions() for (int i = 0; i < current.versions.size(); i++) { auto version = current.versions[i]; + bool valid = false; //NOTE: Flame doesn't care about loaderString, so passing it changes nothing. - if (!validateVersion(version, mcVersion, loaderString)) { - continue; + for(auto& mcVer : m_filter->versions){ + if (validateVersion(version, mcVer.toString(), loaderString)) { + valid = true; + break; + } } - ui->versionSelectionBox->addItem(version.version, QVariant(i)); + if(valid) + ui->versionSelectionBox->addItem(version.version, QVariant(i)); } if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); } diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 0cd13f37..3c71e6fe 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -5,6 +5,7 @@ #include "Application.h" #include "modplatform/ModAPI.h" #include "modplatform/ModIndex.h" +#include "ui/dialogs/FilterModsDialog.h" #include "ui/pages/BasePage.h" #include "ui/pages/modplatform/ModModel.h" @@ -39,6 +40,7 @@ class ModPage : public QWidget, public BasePage { virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool = 0; auto apiProvider() const -> const ModAPI* { return api.get(); }; + auto getFilter() const -> const std::shared_ptr { return m_filter; } auto getCurrent() -> ModPlatform::IndexedPack& { return current; } void updateModVersions(); @@ -52,6 +54,7 @@ class ModPage : public QWidget, public BasePage { void updateSelectionButton(); protected slots: + void filterMods(); void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(QString data); @@ -60,6 +63,10 @@ class ModPage : public QWidget, public BasePage { protected: Ui::ModPage* ui = nullptr; ModDownloadDialog* dialog = nullptr; + + FilterModsDialog filter_dialog; + std::shared_ptr m_filter; + ModPlatform::ListModel* listModel = nullptr; ModPlatform::IndexedPack current; diff --git a/launcher/ui/pages/modplatform/ModPage.ui b/launcher/ui/pages/modplatform/ModPage.ui index 508f1bac..4444583e 100644 --- a/launcher/ui/pages/modplatform/ModPage.ui +++ b/launcher/ui/pages/modplatform/ModPage.ui @@ -51,12 +51,12 @@ - Search and filter... + Search for mods... - + @@ -80,6 +80,13 @@ + + + + Filter options + + +
From c730fd6e5f125cde324d110282ed33ea4b9df136 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 2 Apr 2022 18:34:26 -0300 Subject: [PATCH 240/605] feat: Use version filter when searching mods --- launcher/modplatform/ModAPI.h | 18 ++++++++++++++++-- launcher/modplatform/flame/FlameAPI.h | 2 +- launcher/modplatform/modrinth/ModrinthAPI.h | 13 +------------ launcher/ui/pages/modplatform/ModModel.cpp | 8 +++----- launcher/ui/pages/modplatform/ModModel.h | 3 ++- 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index 1a562172..6a906aa4 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -3,6 +3,8 @@ #include #include +#include "Version.h" + namespace ModPlatform { class ListModel; } @@ -22,7 +24,7 @@ class ModAPI { QString search; QString sorting; ModLoaderType mod_loader; - QString version; + std::list versions; }; virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0; @@ -30,7 +32,7 @@ class ModAPI { struct VersionSearchArgs { QString addonId; - QList mcVersions; + std::list mcVersions; ModLoaderType loader; }; @@ -53,4 +55,16 @@ class ModAPI { } return ""; } + + protected: + inline auto getGameVersionsString(std::list mcVersions) const -> QString + { + QString s; + for(auto& ver : mcVersions){ + s += ver.toString(); + if(ver != mcVersions.back()) + s += ","; + } + return s; + } }; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 8654a693..690ee15c 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -22,7 +22,7 @@ class FlameAPI : public NetworkModAPI { .arg(args.search) .arg(args.sorting) .arg(args.mod_loader) - .arg(args.version); + .arg(getGameVersionsString(args.versions)); }; inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index eefa4a85..0d652568 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -27,7 +27,7 @@ class ModrinthAPI : public NetworkModAPI { .arg(args.search) .arg(args.sorting) .arg(getModLoaderString(args.mod_loader)) - .arg(args.version); + .arg(getGameVersionsString(args.versions)); }; inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override @@ -40,17 +40,6 @@ class ModrinthAPI : public NetworkModAPI { .arg(getModLoaderString(args.loader)); }; - inline auto getGameVersionsString(QList mcVersions) const -> QString - { - QString s; - for(int i = 0; i < mcVersions.count(); i++){ - s += mcVersions.at(i); - if(i < mcVersions.count() - 1) - s += ","; - } - return s; - } - static auto getModLoaderString(ModLoaderType type) -> const QString { if (type == Unspecified) diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index f75d2847..1998fe99 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -72,7 +72,7 @@ void ListModel::performPaginatedSearch() auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); m_parent->apiProvider()->searchMods(this, - { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions().at(0) }); + { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() }); } void ListModel::searchWithTerm(const QString& term, const int sort) @@ -223,9 +223,7 @@ void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) /******** Helpers ********/ -auto ModPlatform::ListModel::getMineVersions() const -> QList +auto ModPlatform::ListModel::getMineVersions() const -> std::list { - return { (dynamic_cast((dynamic_cast(parent()))->m_instance)) - ->getPackProfile() - ->getComponentVersion("net.minecraft") }; + return m_parent->getFilter()->versions; } diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index dbadbeee..1b7601c2 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -7,6 +7,7 @@ #include "net/NetJob.h" class ModPage; +class Version; namespace ModPlatform { @@ -62,7 +63,7 @@ class ListModel : public QAbstractListModel { void requestLogo(QString file, QString url); - inline auto getMineVersions() const -> QList; + inline auto getMineVersions() const -> std::list; protected: ModPage* m_parent; From 5cb0e750936f09513b98a8b0fd57746ca18dc8bc Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 2 Apr 2022 19:21:02 -0300 Subject: [PATCH 241/605] fix(ui): Refresh mod list when changing filtering options --- launcher/modplatform/ModAPI.h | 5 ++--- launcher/modplatform/modrinth/ModrinthAPI.h | 14 ++++++++++++-- launcher/ui/pages/modplatform/ModModel.cpp | 14 ++++++++++---- launcher/ui/pages/modplatform/ModModel.h | 1 + launcher/ui/pages/modplatform/ModPage.cpp | 7 +++++++ 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index 6a906aa4..8e6cd45c 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -61,10 +61,9 @@ class ModAPI { { QString s; for(auto& ver : mcVersions){ - s += ver.toString(); - if(ver != mcVersions.back()) - s += ","; + s += QString("%1,").arg(ver.toString()); } + s.remove(s.length() - 1, 1); //remove last comma return s; } }; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 0d652568..87534ee9 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -22,12 +22,12 @@ class ModrinthAPI : public NetworkModAPI { "limit=25&" "query=%2&" "index=%3&" - "facets=[[\"categories:%4\"],[\"versions:%5\"],[\"project_type:mod\"]]") + "facets=[[\"categories:%4\"],[%5],[\"project_type:mod\"]]") .arg(args.offset) .arg(args.search) .arg(args.sorting) .arg(getModLoaderString(args.mod_loader)) - .arg(getGameVersionsString(args.versions)); + .arg(getGameVersionsArray(args.versions)); }; inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override @@ -40,6 +40,16 @@ class ModrinthAPI : public NetworkModAPI { .arg(getModLoaderString(args.loader)); }; + auto getGameVersionsArray(std::list mcVersions) const -> QString + { + QString s; + for(auto& ver : mcVersions){ + s += QString("\"versions:%1\",").arg(ver.toString()); + } + s.remove(s.length() - 1, 1); //remove last comma + return s; + } + static auto getModLoaderString(ModLoaderType type) -> const QString { if (type == Unspecified) diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 1998fe99..0ff784db 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -75,11 +75,8 @@ void ListModel::performPaginatedSearch() { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() }); } -void ListModel::searchWithTerm(const QString& term, const int sort) +void ListModel::refresh() { - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { return; } - currentSearchTerm = term; - currentSort = sort; if (jobPtr) { jobPtr->abort(); searchState = ResetRequested; @@ -94,6 +91,15 @@ void ListModel::searchWithTerm(const QString& term, const int sort) performPaginatedSearch(); } +void ListModel::searchWithTerm(const QString& term, const int sort) +{ + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { return; } + currentSearchTerm = term; + currentSort = sort; + + refresh(); +} + void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) { if (m_logoMap.contains(logo)) { diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 1b7601c2..d4dc872d 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -34,6 +34,7 @@ class ListModel : public QAbstractListModel { /* Ask the API for more information */ void fetchMore(const QModelIndex& parent) override; + void refresh(); void searchWithTerm(const QString& term, const int sort); void requestModVersions(const ModPlatform::IndexedPack& current); diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 16e44c0d..ece97ef2 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -62,6 +62,13 @@ void ModPage::filterMods() filter_dialog.execWithInstance(static_cast(m_instance)); m_filter = filter_dialog.getFilter(); + + listModel->refresh(); + + if(ui->versionSelectionBox->count() > 0){ + ui->versionSelectionBox->clear(); + updateModVersions(); + } } void ModPage::triggerSearch() From 76dfb7825ade6554095ac3a09b3accdbd4db5138 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 2 Apr 2022 20:08:37 -0300 Subject: [PATCH 242/605] fix: 'All' filter working and get around CF API capabilities --- launcher/modplatform/flame/FlameAPI.h | 6 +- launcher/modplatform/modrinth/ModrinthAPI.h | 4 +- launcher/ui/dialogs/FilterModsDialog.cpp | 31 ++++++++-- launcher/ui/dialogs/FilterModsDialog.h | 17 ++--- launcher/ui/dialogs/FilterModsDialog.ui | 62 +++---------------- launcher/ui/pages/modplatform/ModPage.cpp | 5 +- launcher/ui/pages/modplatform/ModPage.h | 2 +- .../pages/modplatform/flame/FlameModPage.cpp | 19 ++++++ .../ui/pages/modplatform/flame/FlameModPage.h | 2 + 9 files changed, 74 insertions(+), 74 deletions(-) diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 690ee15c..9bcc357e 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -6,6 +6,8 @@ class FlameAPI : public NetworkModAPI { private: inline auto getModSearchURL(SearchArgs& args) const -> QString override { + auto gameVersionStr = args.versions.size() != 0 ? QString("gameVersion=%1").arg(args.versions.front().toString()) : QString(); + return QString( "https://addons-ecs.forgesvc.net/api/v2/addon/search?" "gameId=432&" @@ -17,12 +19,12 @@ class FlameAPI : public NetworkModAPI { "searchFilter=%2&" "sort=%3&" "modLoaderType=%4&" - "gameVersion=%5") + "%5") .arg(args.offset) .arg(args.search) .arg(args.sorting) .arg(args.mod_loader) - .arg(getGameVersionsString(args.versions)); + .arg(gameVersionStr); }; inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 87534ee9..6604d772 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -22,7 +22,7 @@ class ModrinthAPI : public NetworkModAPI { "limit=25&" "query=%2&" "index=%3&" - "facets=[[\"categories:%4\"],[%5],[\"project_type:mod\"]]") + "facets=[[\"categories:%4\"],%5[\"project_type:mod\"]]") .arg(args.offset) .arg(args.search) .arg(args.sorting) @@ -47,7 +47,7 @@ class ModrinthAPI : public NetworkModAPI { s += QString("\"versions:%1\",").arg(ver.toString()); } s.remove(s.length() - 1, 1); //remove last comma - return s; + return s.isEmpty() ? QString() : QString("[%1],").arg(s); } static auto getModLoaderString(ModLoaderType type) -> const QString diff --git a/launcher/ui/dialogs/FilterModsDialog.cpp b/launcher/ui/dialogs/FilterModsDialog.cpp index a36f4ba4..bf4999e0 100644 --- a/launcher/ui/dialogs/FilterModsDialog.cpp +++ b/launcher/ui/dialogs/FilterModsDialog.cpp @@ -9,7 +9,7 @@ FilterModsDialog::FilterModsDialog(Version def, QWidget* parent) m_mcVersion_buttons.addButton(ui->strictVersionButton, VersionButtonID::Strict); m_mcVersion_buttons.addButton(ui->majorVersionButton, VersionButtonID::Major); m_mcVersion_buttons.addButton(ui->allVersionsButton, VersionButtonID::All); - m_mcVersion_buttons.addButton(ui->betweenVersionsButton, VersionButtonID::Between); + //m_mcVersion_buttons.addButton(ui->betweenVersionsButton, VersionButtonID::Between); connect(&m_mcVersion_buttons, SIGNAL(idClicked(int)), this, SLOT(onVersionFilterChanged(int))); @@ -31,18 +31,38 @@ int FilterModsDialog::execWithInstance(MinecraftInstance* instance) tr("Major varsion match (= %1.%2.x)").arg(mcVersionSplit[0], mcVersionSplit[1])); ui->allVersionsButton->setText( tr("Any version match")); - ui->betweenVersionsButton->setText( - tr("Between two versions")); + //ui->betweenVersionsButton->setText( + // tr("Between two versions")); int ret = QDialog::exec(); m_instance = nullptr; return ret; } +void FilterModsDialog::disableVersionButton(VersionButtonID id) +{ + switch(id){ + case(VersionButtonID::Strict): + ui->strictVersionButton->setEnabled(false); + break; + case(VersionButtonID::Major): + ui->majorVersionButton->setEnabled(false); + break; + case(VersionButtonID::All): + ui->allVersionsButton->setEnabled(false); + break; + case(VersionButtonID::Between): + // ui->betweenVersionsButton->setEnabled(false); + break; + default: + break; + } +} + void FilterModsDialog::onVersionFilterChanged(int id) { - ui->lowerVersionComboBox->setEnabled(id == VersionButtonID::Between); - ui->upperVersionComboBox->setEnabled(id == VersionButtonID::Between); + //ui->lowerVersionComboBox->setEnabled(id == VersionButtonID::Between); + //ui->upperVersionComboBox->setEnabled(id == VersionButtonID::Between); auto versionSplit = mcVersionStr().split("."); int index = 0; @@ -60,6 +80,7 @@ void FilterModsDialog::onVersionFilterChanged(int id) } break; case(VersionButtonID::All): + // Empty list to avoid enumerating all versions :P break; case(VersionButtonID::Between): // TODO diff --git a/launcher/ui/dialogs/FilterModsDialog.h b/launcher/ui/dialogs/FilterModsDialog.h index b119e8e5..3684b03c 100644 --- a/launcher/ui/dialogs/FilterModsDialog.h +++ b/launcher/ui/dialogs/FilterModsDialog.h @@ -17,6 +17,13 @@ class FilterModsDialog : public QDialog { Q_OBJECT public: + enum VersionButtonID { + Strict = 0, + Major = 1, + All = 2, + Between = 3 + }; + struct Filter { std::list versions; }; @@ -29,16 +36,12 @@ public: int execWithInstance(MinecraftInstance* instance); + /// By default all buttons are enabled + void disableVersionButton(VersionButtonID); + auto getFilter() -> std::shared_ptr { return m_filter; } private: - enum VersionButtonID { - Strict = 0, - Major = 1, - All = 2, - Between = 3 - }; - inline auto mcVersionStr() const -> QString { return m_instance ? m_instance->getPackProfile()->getComponentVersion("net.minecraft") : ""; } inline auto mcVersion() const -> Version { return { mcVersionStr() }; } diff --git a/launcher/ui/dialogs/FilterModsDialog.ui b/launcher/ui/dialogs/FilterModsDialog.ui index da368aac..67a63bfd 100644 --- a/launcher/ui/dialogs/FilterModsDialog.ui +++ b/launcher/ui/dialogs/FilterModsDialog.ui @@ -10,15 +10,17 @@ 0 0 345 - 323 + 169 - - Copy Instance + + + 0 + 0 + - - - :/icons/toolbar/copy:/icons/toolbar/copy + + Filter options true @@ -78,59 +80,11 @@
- - - - - - - - BetweenVersions - - - - - - - From - - - - - - - - - - To - - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index ece97ef2..ba56c3b8 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -64,7 +64,6 @@ void ModPage::filterMods() m_filter = filter_dialog.getFilter(); listModel->refresh(); - if(ui->versionSelectionBox->count() > 0){ ui->versionSelectionBox->clear(); updateModVersions(); @@ -164,14 +163,14 @@ void ModPage::updateModVersions() for (int i = 0; i < current.versions.size(); i++) { auto version = current.versions[i]; bool valid = false; - //NOTE: Flame doesn't care about loaderString, so passing it changes nothing. for(auto& mcVer : m_filter->versions){ + //NOTE: Flame doesn't care about loaderString, so passing it changes nothing. if (validateVersion(version, mcVer.toString(), loaderString)) { valid = true; break; } } - if(valid) + if(valid || m_filter->versions.size() == 0) ui->versionSelectionBox->addItem(version.version, QVariant(i)); } if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); } diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 3c71e6fe..0befc7c3 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -54,7 +54,7 @@ class ModPage : public QWidget, public BasePage { void updateSelectionButton(); protected slots: - void filterMods(); + virtual void filterMods(); void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(QString data); diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 864ae8e6..5398bda3 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -67,6 +67,25 @@ auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString min return ver.mcVersion.contains(mineVer); } +// We override this so that it refreshes correctly, otherwise it wouldn't show +// any mod on the mod list, because the CF API does not support it :( +void FlameModPage::filterMods() +{ + filter_dialog.execWithInstance(static_cast(m_instance)); + + int prev_size = m_filter->versions.size(); + m_filter = filter_dialog.getFilter(); + int new_size = m_filter->versions.size(); + + if(new_size <= 1 && new_size != prev_size) + listModel->refresh(); + + if(ui->versionSelectionBox->count() > 0){ + ui->versionSelectionBox->clear(); + updateModVersions(); + } +} + // 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... diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index dc58fd7f..7078e889 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -56,5 +56,7 @@ class FlameModPage : public ModPage { auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override; + void filterMods() override; + auto shouldDisplay() const -> bool override; }; From 63bce0464895236a043aa8a98e2905ab1bf34cc1 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 3 Apr 2022 10:21:48 -0300 Subject: [PATCH 243/605] fix: Polish usage in some cases Also fiz some typos --- launcher/ui/dialogs/FilterModsDialog.cpp | 42 +++++++++++++++---- launcher/ui/dialogs/FilterModsDialog.h | 8 ++++ launcher/ui/pages/modplatform/ModPage.cpp | 19 +++++---- launcher/ui/pages/modplatform/ModPage.h | 2 +- .../pages/modplatform/flame/FlameModPage.cpp | 20 ++++----- 5 files changed, 64 insertions(+), 27 deletions(-) diff --git a/launcher/ui/dialogs/FilterModsDialog.cpp b/launcher/ui/dialogs/FilterModsDialog.cpp index bf4999e0..9440d897 100644 --- a/launcher/ui/dialogs/FilterModsDialog.cpp +++ b/launcher/ui/dialogs/FilterModsDialog.cpp @@ -2,7 +2,7 @@ #include "ui_FilterModsDialog.h" FilterModsDialog::FilterModsDialog(Version def, QWidget* parent) - : QDialog(parent), m_filter(new Filter()), ui(new Ui::FilterModsDialog) + : QDialog(parent), m_filter(new Filter()), m_internal_filter(new Filter()), ui(new Ui::FilterModsDialog) { ui->setupUi(this); @@ -13,28 +13,44 @@ FilterModsDialog::FilterModsDialog(Version def, QWidget* parent) connect(&m_mcVersion_buttons, SIGNAL(idClicked(int)), this, SLOT(onVersionFilterChanged(int))); - m_filter->versions.push_front(def); + m_internal_filter->versions.push_front(def); + commitChanges(); } int FilterModsDialog::execWithInstance(MinecraftInstance* instance) { m_instance = instance; + auto* pressed_button = m_mcVersion_buttons.checkedButton(); + // Fix first openening behaviour - onVersionFilterChanged(0); + onVersionFilterChanged(m_previous_mcVersion_id); auto mcVersionSplit = mcVersionStr().split("."); ui->strictVersionButton->setText( tr("Strict match (= %1)").arg(mcVersionStr())); ui->majorVersionButton->setText( - tr("Major varsion match (= %1.%2.x)").arg(mcVersionSplit[0], mcVersionSplit[1])); + tr("Major version match (= %1.%2.x)").arg(mcVersionSplit[0], mcVersionSplit[1])); ui->allVersionsButton->setText( - tr("Any version match")); + tr("Any version")); //ui->betweenVersionsButton->setText( // tr("Between two versions")); int ret = QDialog::exec(); + + if(ret == QDialog::DialogCode::Accepted){ + // If there's no change, let's sey it's a cancel to the caller + if(*m_internal_filter.get() == *m_filter.get()) + return QDialog::DialogCode::Rejected; + + m_previous_mcVersion_id = (VersionButtonID) m_mcVersion_buttons.checkedId(); + commitChanges(); + } else { + pressed_button->click(); + revertChanges(); + } + m_instance = nullptr; return ret; } @@ -59,6 +75,16 @@ void FilterModsDialog::disableVersionButton(VersionButtonID id) } } +// Do deep copy +void FilterModsDialog::commitChanges() +{ + m_filter->versions = m_internal_filter->versions; +} +void FilterModsDialog::revertChanges() +{ + m_internal_filter->versions = m_filter->versions; +} + void FilterModsDialog::onVersionFilterChanged(int id) { //ui->lowerVersionComboBox->setEnabled(id == VersionButtonID::Between); @@ -67,15 +93,15 @@ void FilterModsDialog::onVersionFilterChanged(int id) auto versionSplit = mcVersionStr().split("."); int index = 0; - m_filter->versions.clear(); + m_internal_filter->versions.clear(); switch(id){ case(VersionButtonID::Strict): - m_filter->versions.push_front(mcVersion()); + m_internal_filter->versions.push_front(mcVersion()); break; case(VersionButtonID::Major): for(auto i = Version(QString("%1.%2").arg(versionSplit[0], versionSplit[1])); i <= mcVersion(); index++){ - m_filter->versions.push_front(i); + m_internal_filter->versions.push_front(i); i = Version(QString("%1.%2.%3").arg(versionSplit[0], versionSplit[1], QString("%1").arg(index))); } break; diff --git a/launcher/ui/dialogs/FilterModsDialog.h b/launcher/ui/dialogs/FilterModsDialog.h index 3684b03c..fac28ca4 100644 --- a/launcher/ui/dialogs/FilterModsDialog.h +++ b/launcher/ui/dialogs/FilterModsDialog.h @@ -26,9 +26,13 @@ public: struct Filter { std::list versions; + + bool operator==(const Filter& other) const { return versions == other.versions; } + bool operator!=(const Filter& other) const { return !(*this == other); } }; std::shared_ptr m_filter; + std::shared_ptr m_internal_filter; public: explicit FilterModsDialog(Version def, QWidget* parent = nullptr); @@ -45,6 +49,9 @@ private: inline auto mcVersionStr() const -> QString { return m_instance ? m_instance->getPackProfile()->getComponentVersion("net.minecraft") : ""; } inline auto mcVersion() const -> Version { return { mcVersionStr() }; } + void commitChanges(); + void revertChanges(); + private slots: void onVersionFilterChanged(int id); @@ -54,4 +61,5 @@ private: MinecraftInstance* m_instance = nullptr; QButtonGroup m_mcVersion_buttons; + VersionButtonID m_previous_mcVersion_id = VersionButtonID::Strict; }; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index ba56c3b8..37f68291 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -59,14 +59,15 @@ auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool void ModPage::filterMods() { - filter_dialog.execWithInstance(static_cast(m_instance)); - + auto ret = filter_dialog.execWithInstance(static_cast(m_instance)); m_filter = filter_dialog.getFilter(); - listModel->refresh(); - if(ui->versionSelectionBox->count() > 0){ + if(ret == QDialog::DialogCode::Accepted){ + listModel->refresh(); + + int prev_count = ui->versionSelectionBox->count(); ui->versionSelectionBox->clear(); - updateModVersions(); + updateModVersions(prev_count); } } @@ -152,7 +153,7 @@ void ModPage::retranslate() ui->retranslateUi(this); } -void ModPage::updateModVersions() +void ModPage::updateModVersions(int prev_count) { auto packProfile = (dynamic_cast(m_instance))->getPackProfile(); @@ -173,9 +174,11 @@ void ModPage::updateModVersions() if(valid || m_filter->versions.size() == 0) ui->versionSelectionBox->addItem(version.version, QVariant(i)); } - if (ui->versionSelectionBox->count() == 0) { ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); } + if (ui->versionSelectionBox->count() == 0 && prev_count != 0) { + ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); + ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); + } - ui->modSelectionButton->setText(tr("Cannot select invalid version :(")); updateSelectionButton(); } diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 0befc7c3..06581ab2 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -43,7 +43,7 @@ class ModPage : public QWidget, public BasePage { auto getFilter() const -> const std::shared_ptr { return m_filter; } auto getCurrent() -> ModPlatform::IndexedPack& { return current; } - void updateModVersions(); + void updateModVersions(int prev_count = -1); void openedImpl() override; auto eventFilter(QObject* watched, QEvent* event) -> bool override; diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 5398bda3..6e666c4c 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -67,22 +67,22 @@ auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString min return ver.mcVersion.contains(mineVer); } -// We override this so that it refreshes correctly, otherwise it wouldn't show -// any mod on the mod list, because the CF API does not support it :( +// We override this so that it refreshes correctly, otherwise it would show +// invalid mods on the mod list, because the API would return mods for the +// wrong mod loader :( void FlameModPage::filterMods() { - filter_dialog.execWithInstance(static_cast(m_instance)); - - int prev_size = m_filter->versions.size(); + auto ret = filter_dialog.execWithInstance(static_cast(m_instance)); m_filter = filter_dialog.getFilter(); - int new_size = m_filter->versions.size(); - if(new_size <= 1 && new_size != prev_size) - listModel->refresh(); + if(ret == QDialog::DialogCode::Accepted){ + // CF API can't handle well this + if(!m_filter->versions.empty()) + listModel->refresh(); - if(ui->versionSelectionBox->count() > 0){ + int prev_count = ui->versionSelectionBox->count(); ui->versionSelectionBox->clear(); - updateModVersions(); + updateModVersions(prev_count); } } From 277de4120052b2630850a18969a56ee92e8c2c63 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 14 Apr 2022 10:27:03 -0300 Subject: [PATCH 244/605] rework: make the filter as a tabbed widget in the dialog itself Still needs a clear indication that the filter only applies after you click the search button... --- launcher/ui/dialogs/FilterModsDialog.ui | 135 ------------------ launcher/ui/pages/modplatform/ModModel.cpp | 9 +- launcher/ui/pages/modplatform/ModModel.h | 2 +- launcher/ui/pages/modplatform/ModPage.cpp | 30 ++-- launcher/ui/pages/modplatform/ModPage.h | 8 +- launcher/ui/pages/modplatform/ModPage.ui | 27 ++-- .../pages/modplatform/flame/FlameModPage.cpp | 19 --- .../ui/pages/modplatform/flame/FlameModPage.h | 2 - .../ModFilterWidget.cpp} | 73 ++++------ .../ModFilterWidget.h} | 26 ++-- launcher/ui/widgets/ModFilterWidget.ui | 54 +++++++ 11 files changed, 138 insertions(+), 247 deletions(-) delete mode 100644 launcher/ui/dialogs/FilterModsDialog.ui rename launcher/ui/{dialogs/FilterModsDialog.cpp => widgets/ModFilterWidget.cpp} (57%) rename launcher/ui/{dialogs/FilterModsDialog.h => widgets/ModFilterWidget.h} (64%) create mode 100644 launcher/ui/widgets/ModFilterWidget.ui diff --git a/launcher/ui/dialogs/FilterModsDialog.ui b/launcher/ui/dialogs/FilterModsDialog.ui deleted file mode 100644 index 67a63bfd..00000000 --- a/launcher/ui/dialogs/FilterModsDialog.ui +++ /dev/null @@ -1,135 +0,0 @@ - - - FilterModsDialog - - - Qt::ApplicationModal - - - - 0 - 0 - 345 - 169 - - - - - 0 - 0 - - - - Filter options - - - true - - - true - - - - - - - 0 - 0 - - - - Minecraft versions - - - false - - - false - - - - - - QLayout::SetDefaultConstraint - - - - - - - StrictVersion - - - true - - - - - - - MajorVersion - - - - - - - AllVersions - - - - - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - buttonBox - accepted() - FilterModsDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - FilterModsDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 0ff784db..da0331b5 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -91,9 +91,14 @@ void ListModel::refresh() performPaginatedSearch(); } -void ListModel::searchWithTerm(const QString& term, const int sort) +void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed) { - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { return; } + if (currentSearchTerm == term + && currentSearchTerm.isNull() == term.isNull() + && currentSort == sort + && !filter_changed) + { return; } + currentSearchTerm = term; currentSort = sort; diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index d4dc872d..d460cff2 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -35,7 +35,7 @@ class ListModel : public QAbstractListModel { /* Ask the API for more information */ void fetchMore(const QModelIndex& parent) override; void refresh(); - void searchWithTerm(const QString& term, const int sort); + void searchWithTerm(const QString& term, const int sort, const bool filter_changed); void requestModVersions(const ModPlatform::IndexedPack& current); virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 37f68291..57c2e45d 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -13,7 +13,7 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) , m_instance(instance) , ui(new Ui::ModPage) , dialog(dialog) - , filter_dialog(static_cast(instance)->getPackProfile()->getComponentVersion("net.minecraft"), this) + , filter_widget(static_cast(instance)->getPackProfile()->getComponentVersion("net.minecraft"), this) , api(api) { ui->setupUi(this); @@ -24,7 +24,10 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); - m_filter = filter_dialog.getFilter(); + ui->gridLayout_3->addWidget(&filter_widget, 0, 0, 1, ui->gridLayout_3->columnCount()); + + filter_widget.setInstance(static_cast(m_instance)); + m_filter = filter_widget.getFilter(); } ModPage::~ModPage() @@ -59,21 +62,22 @@ auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool void ModPage::filterMods() { - auto ret = filter_dialog.execWithInstance(static_cast(m_instance)); - m_filter = filter_dialog.getFilter(); - - if(ret == QDialog::DialogCode::Accepted){ - listModel->refresh(); - - int prev_count = ui->versionSelectionBox->count(); - ui->versionSelectionBox->clear(); - updateModVersions(prev_count); - } + filter_widget.setHidden(!filter_widget.isHidden()); } void ModPage::triggerSearch() { - listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); + auto changed = filter_widget.changed(); + m_filter = filter_widget.getFilter(); + + if(changed){ + ui->packView->clearSelection(); + ui->packDescription->clear(); + ui->versionSelectionBox->clear(); + updateSelectionButton(); + } + + listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), changed); } void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 06581ab2..85aaede9 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -5,9 +5,9 @@ #include "Application.h" #include "modplatform/ModAPI.h" #include "modplatform/ModIndex.h" -#include "ui/dialogs/FilterModsDialog.h" #include "ui/pages/BasePage.h" #include "ui/pages/modplatform/ModModel.h" +#include "ui/widgets/ModFilterWidget.h" class ModDownloadDialog; @@ -40,7 +40,7 @@ class ModPage : public QWidget, public BasePage { virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool = 0; auto apiProvider() const -> const ModAPI* { return api.get(); }; - auto getFilter() const -> const std::shared_ptr { return m_filter; } + auto getFilter() const -> const std::shared_ptr { return m_filter; } auto getCurrent() -> ModPlatform::IndexedPack& { return current; } void updateModVersions(int prev_count = -1); @@ -64,8 +64,8 @@ class ModPage : public QWidget, public BasePage { Ui::ModPage* ui = nullptr; ModDownloadDialog* dialog = nullptr; - FilterModsDialog filter_dialog; - std::shared_ptr m_filter; + ModFilterWidget filter_widget; + std::shared_ptr m_filter; ModPlatform::ListModel* listModel = nullptr; ModPlatform::IndexedPack current; diff --git a/launcher/ui/pages/modplatform/ModPage.ui b/launcher/ui/pages/modplatform/ModPage.ui index 4444583e..afcd9bb7 100644 --- a/launcher/ui/pages/modplatform/ModPage.ui +++ b/launcher/ui/pages/modplatform/ModPage.ui @@ -11,7 +11,7 @@ - + @@ -41,7 +41,7 @@ - + Search @@ -55,7 +55,7 @@ - + @@ -80,15 +80,22 @@ - - - - Filter options - - - + + + + Filter options + + + + + + + Qt::Vertical + + + diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 6e666c4c..864ae8e6 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -67,25 +67,6 @@ auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString min return ver.mcVersion.contains(mineVer); } -// We override this so that it refreshes correctly, otherwise it would show -// invalid mods on the mod list, because the API would return mods for the -// wrong mod loader :( -void FlameModPage::filterMods() -{ - auto ret = filter_dialog.execWithInstance(static_cast(m_instance)); - m_filter = filter_dialog.getFilter(); - - if(ret == QDialog::DialogCode::Accepted){ - // CF API can't handle well this - if(!m_filter->versions.empty()) - listModel->refresh(); - - int prev_count = ui->versionSelectionBox->count(); - ui->versionSelectionBox->clear(); - updateModVersions(prev_count); - } -} - // 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... diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 7078e889..dc58fd7f 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -56,7 +56,5 @@ class FlameModPage : public ModPage { auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override; - void filterMods() override; - auto shouldDisplay() const -> bool override; }; diff --git a/launcher/ui/dialogs/FilterModsDialog.cpp b/launcher/ui/widgets/ModFilterWidget.cpp similarity index 57% rename from launcher/ui/dialogs/FilterModsDialog.cpp rename to launcher/ui/widgets/ModFilterWidget.cpp index 9440d897..339ecb4b 100644 --- a/launcher/ui/dialogs/FilterModsDialog.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -1,31 +1,28 @@ -#include "FilterModsDialog.h" -#include "ui_FilterModsDialog.h" +#include "ModFilterWidget.h" +#include "ui_ModFilterWidget.h" -FilterModsDialog::FilterModsDialog(Version def, QWidget* parent) - : QDialog(parent), m_filter(new Filter()), m_internal_filter(new Filter()), ui(new Ui::FilterModsDialog) +ModFilterWidget::ModFilterWidget(Version def, QWidget* parent) + : QTabWidget(parent), m_filter(new Filter()), ui(new Ui::ModFilterWidget) { ui->setupUi(this); m_mcVersion_buttons.addButton(ui->strictVersionButton, VersionButtonID::Strict); + ui->strictVersionButton->click(); m_mcVersion_buttons.addButton(ui->majorVersionButton, VersionButtonID::Major); m_mcVersion_buttons.addButton(ui->allVersionsButton, VersionButtonID::All); //m_mcVersion_buttons.addButton(ui->betweenVersionsButton, VersionButtonID::Between); connect(&m_mcVersion_buttons, SIGNAL(idClicked(int)), this, SLOT(onVersionFilterChanged(int))); - m_internal_filter->versions.push_front(def); - commitChanges(); + m_filter->versions.push_front(def); + + setHidden(true); } -int FilterModsDialog::execWithInstance(MinecraftInstance* instance) +void ModFilterWidget::setInstance(MinecraftInstance* instance) { m_instance = instance; - auto* pressed_button = m_mcVersion_buttons.checkedButton(); - - // Fix first openening behaviour - onVersionFilterChanged(m_previous_mcVersion_id); - auto mcVersionSplit = mcVersionStr().split("."); ui->strictVersionButton->setText( @@ -36,26 +33,15 @@ int FilterModsDialog::execWithInstance(MinecraftInstance* instance) tr("Any version")); //ui->betweenVersionsButton->setText( // tr("Between two versions")); - - int ret = QDialog::exec(); - - if(ret == QDialog::DialogCode::Accepted){ - // If there's no change, let's sey it's a cancel to the caller - if(*m_internal_filter.get() == *m_filter.get()) - return QDialog::DialogCode::Rejected; - - m_previous_mcVersion_id = (VersionButtonID) m_mcVersion_buttons.checkedId(); - commitChanges(); - } else { - pressed_button->click(); - revertChanges(); - } - - m_instance = nullptr; - return ret; } -void FilterModsDialog::disableVersionButton(VersionButtonID id) +auto ModFilterWidget::getFilter() -> std::shared_ptr +{ + m_last_version_id = m_version_id; + return m_filter; +} + +void ModFilterWidget::disableVersionButton(VersionButtonID id) { switch(id){ case(VersionButtonID::Strict): @@ -75,17 +61,7 @@ void FilterModsDialog::disableVersionButton(VersionButtonID id) } } -// Do deep copy -void FilterModsDialog::commitChanges() -{ - m_filter->versions = m_internal_filter->versions; -} -void FilterModsDialog::revertChanges() -{ - m_internal_filter->versions = m_filter->versions; -} - -void FilterModsDialog::onVersionFilterChanged(int id) +void ModFilterWidget::onVersionFilterChanged(int id) { //ui->lowerVersionComboBox->setEnabled(id == VersionButtonID::Between); //ui->upperVersionComboBox->setEnabled(id == VersionButtonID::Between); @@ -93,15 +69,18 @@ void FilterModsDialog::onVersionFilterChanged(int id) auto versionSplit = mcVersionStr().split("."); int index = 0; - m_internal_filter->versions.clear(); + auto cast_id = (VersionButtonID) id; + m_version_id = cast_id; - switch(id){ + m_filter->versions.clear(); + + switch(cast_id){ case(VersionButtonID::Strict): - m_internal_filter->versions.push_front(mcVersion()); + m_filter->versions.push_front(mcVersion()); break; case(VersionButtonID::Major): for(auto i = Version(QString("%1.%2").arg(versionSplit[0], versionSplit[1])); i <= mcVersion(); index++){ - m_internal_filter->versions.push_front(i); + m_filter->versions.push_front(i); i = Version(QString("%1.%2.%3").arg(versionSplit[0], versionSplit[1], QString("%1").arg(index))); } break; @@ -111,12 +90,10 @@ void FilterModsDialog::onVersionFilterChanged(int id) case(VersionButtonID::Between): // TODO break; - default: - break; } } -FilterModsDialog::~FilterModsDialog() +ModFilterWidget::~ModFilterWidget() { delete ui; } diff --git a/launcher/ui/dialogs/FilterModsDialog.h b/launcher/ui/widgets/ModFilterWidget.h similarity index 64% rename from launcher/ui/dialogs/FilterModsDialog.h rename to launcher/ui/widgets/ModFilterWidget.h index fac28ca4..5348882f 100644 --- a/launcher/ui/dialogs/FilterModsDialog.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include "Version.h" @@ -10,10 +10,10 @@ class MinecraftInstance; namespace Ui { -class FilterModsDialog; +class ModFilterWidget; } -class FilterModsDialog : public QDialog +class ModFilterWidget : public QTabWidget { Q_OBJECT public: @@ -32,34 +32,34 @@ public: }; std::shared_ptr m_filter; - std::shared_ptr m_internal_filter; public: - explicit FilterModsDialog(Version def, QWidget* parent = nullptr); - ~FilterModsDialog(); + explicit ModFilterWidget(Version def, QWidget* parent = nullptr); + ~ModFilterWidget(); - int execWithInstance(MinecraftInstance* instance); + void setInstance(MinecraftInstance* instance); /// By default all buttons are enabled void disableVersionButton(VersionButtonID); - auto getFilter() -> std::shared_ptr { return m_filter; } + auto getFilter() -> std::shared_ptr; + auto changed() const -> bool { return m_last_version_id != m_version_id; } private: inline auto mcVersionStr() const -> QString { return m_instance ? m_instance->getPackProfile()->getComponentVersion("net.minecraft") : ""; } inline auto mcVersion() const -> Version { return { mcVersionStr() }; } - void commitChanges(); - void revertChanges(); - private slots: void onVersionFilterChanged(int id); private: - Ui::FilterModsDialog* ui; + Ui::ModFilterWidget* ui; MinecraftInstance* m_instance = nullptr; QButtonGroup m_mcVersion_buttons; - VersionButtonID m_previous_mcVersion_id = VersionButtonID::Strict; + + /* Used to tell if the filter was changed since the last getFilter() call */ + VersionButtonID m_last_version_id = VersionButtonID::Strict; + VersionButtonID m_version_id = VersionButtonID::Strict; }; diff --git a/launcher/ui/widgets/ModFilterWidget.ui b/launcher/ui/widgets/ModFilterWidget.ui new file mode 100644 index 00000000..ad1090e2 --- /dev/null +++ b/launcher/ui/widgets/ModFilterWidget.ui @@ -0,0 +1,54 @@ + + + ModFilterWidget + + + + 0 + 0 + 400 + 300 + + + + + 0 + 0 + + + + + Minecraft versions + + + + + + + + allVersions + + + + + + + strictVersion + + + + + + + majorVersion + + + + + + + + + + + From 5f15f51610f861521afdb295df0de6d9407ba951 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 14 Apr 2022 10:52:23 -0300 Subject: [PATCH 245/605] ui: underline search button text when changing filters This hopefully makes it easier to the user to know that their changes will only apply after hitting the search button. I tried setting the background color, but it seems more unreliable on cross-platform than underlining. Also, it could be worse for daltonic people, so I don't know what to do :( --- launcher/CMakeLists.txt | 7 +++---- launcher/ui/pages/modplatform/ModPage.cpp | 7 +++++++ launcher/ui/widgets/ModFilterWidget.cpp | 12 +++++++++++- launcher/ui/widgets/ModFilterWidget.h | 4 ++++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 8579071a..0ab398e8 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -824,9 +824,6 @@ SET(LAUNCHER_SOURCES ui/dialogs/SkinUploadDialog.h ui/dialogs/ModDownloadDialog.cpp ui/dialogs/ModDownloadDialog.h - ui/dialogs/FilterModsDialog.cpp - ui/dialogs/FilterModsDialog.h - # GUI - widgets ui/widgets/Common.cpp @@ -851,6 +848,8 @@ SET(LAUNCHER_SOURCES ui/widgets/LogView.h ui/widgets/MCModInfoFrame.cpp ui/widgets/MCModInfoFrame.h + ui/widgets/ModFilterWidget.cpp + ui/widgets/ModFilterWidget.h ui/widgets/ModListView.cpp ui/widgets/ModListView.h ui/widgets/PageContainer.cpp @@ -909,6 +908,7 @@ qt5_wrap_ui(LAUNCHER_UI ui/widgets/InstanceCardWidget.ui ui/widgets/CustomCommands.ui ui/widgets/MCModInfoFrame.ui + ui/widgets/ModFilterWidget.ui ui/dialogs/CopyInstanceDialog.ui ui/dialogs/ProfileSetupDialog.ui ui/dialogs/ProgressDialog.ui @@ -925,7 +925,6 @@ qt5_wrap_ui(LAUNCHER_UI ui/dialogs/LoginDialog.ui ui/dialogs/EditAccountDialog.ui ui/dialogs/ReviewMessageBox.ui - ui/dialogs/FilterModsDialog.ui ) qt5_add_resources(LAUNCHER_RESOURCES diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 57c2e45d..29f6b601 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -28,6 +28,13 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) filter_widget.setInstance(static_cast(m_instance)); m_filter = filter_widget.getFilter(); + + connect(&filter_widget, &ModFilterWidget::filterChanged, this, [&]{ + ui->searchButton->setStyleSheet("text-decoration: underline"); + }); + connect(&filter_widget, &ModFilterWidget::filterUnchanged, this, [&]{ + ui->searchButton->setStyleSheet("text-decoration: none"); + }); } ModPage::~ModPage() diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index 339ecb4b..ffc8d05d 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -38,6 +38,7 @@ void ModFilterWidget::setInstance(MinecraftInstance* instance) auto ModFilterWidget::getFilter() -> std::shared_ptr { m_last_version_id = m_version_id; + emit filterUnchanged(); return m_filter; } @@ -70,7 +71,11 @@ void ModFilterWidget::onVersionFilterChanged(int id) int index = 0; auto cast_id = (VersionButtonID) id; - m_version_id = cast_id; + if (cast_id != m_version_id) { + m_version_id = cast_id; + } else { + return; + } m_filter->versions.clear(); @@ -91,6 +96,11 @@ void ModFilterWidget::onVersionFilterChanged(int id) // TODO break; } + + if(changed()) + emit filterChanged(); + else + emit filterUnchanged(); } ModFilterWidget::~ModFilterWidget() diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index 5348882f..334fc672 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -52,6 +52,10 @@ private: private slots: void onVersionFilterChanged(int id); +public: signals: + void filterChanged(); + void filterUnchanged(); + private: Ui::ModFilterWidget* ui; From 8e9eca6a970bf098c1045658368dc66b39e438ed Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 15 Apr 2022 08:27:40 -0300 Subject: [PATCH 246/605] ui: resize mod download dialog using its parents geometry --- launcher/ui/dialogs/ModDownloadDialog.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 9aac4145..d02ea476 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -22,7 +22,9 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr &mods : QDialog(parent), mods(mods), m_instance(instance) { setObjectName(QStringLiteral("ModDownloadDialog")); - resize(400, 347); + + resize(std::max(0.5*parent->width(), 400.0), std::max(0.75*parent->height(), 400.0)); + m_verticalLayout = new QVBoxLayout(this); m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); From 8406c7f431009d7e81b0b0e9e1daddcaf82d8324 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 4 Apr 2022 19:30:27 -0400 Subject: [PATCH 247/605] Add option to install mod loader during instance creation --- launcher/InstanceCreationTask.cpp | 11 + launcher/InstanceCreationTask.h | 4 + launcher/ui/pages/modplatform/VanillaPage.cpp | 71 +++- launcher/ui/pages/modplatform/VanillaPage.h | 7 + launcher/ui/pages/modplatform/VanillaPage.ui | 311 ++++++++++++------ 5 files changed, 303 insertions(+), 101 deletions(-) diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp index 4c37bd7f..24bc5f46 100644 --- a/launcher/InstanceCreationTask.cpp +++ b/launcher/InstanceCreationTask.cpp @@ -9,6 +9,15 @@ InstanceCreationTask::InstanceCreationTask(BaseVersionPtr version) { m_version = version; + m_usingLoader = false; +} + +InstanceCreationTask::InstanceCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loaderVersion) +{ + m_version = version; + m_usingLoader = true; + m_loader = loader; + m_loaderVersion = loaderVersion; } void InstanceCreationTask::executeTask() @@ -21,6 +30,8 @@ void InstanceCreationTask::executeTask() auto components = inst.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", m_version->descriptor(), true); + if(m_usingLoader) + components->setComponentVersion(m_loader, m_loaderVersion->descriptor(), true); inst.setName(m_instName); inst.setIconKey(m_instIcon); instanceSettings->resumeSave(); diff --git a/launcher/InstanceCreationTask.h b/launcher/InstanceCreationTask.h index 54997116..23367c3f 100644 --- a/launcher/InstanceCreationTask.h +++ b/launcher/InstanceCreationTask.h @@ -12,6 +12,7 @@ class InstanceCreationTask : public InstanceTask Q_OBJECT public: explicit InstanceCreationTask(BaseVersionPtr version); + explicit InstanceCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loaderVersion); protected: //! Entry point for tasks. @@ -19,4 +20,7 @@ protected: private: /* data */ BaseVersionPtr m_version; + bool m_usingLoader; + QString m_loader; + BaseVersionPtr m_loaderVersion; }; diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/VanillaPage.cpp index c691128f..959e695c 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.cpp +++ b/launcher/ui/pages/modplatform/VanillaPage.cpp @@ -59,6 +59,11 @@ VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent) connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); connect(ui->refreshBtn, &QPushButton::clicked, this, &VanillaPage::refresh); + + connect(ui->loaderVersionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedLoaderVersion); + connect(ui->loaderBtnGroup, &QButtonGroup::idToggled, this, &VanillaPage::loaderFilterChanged); + connect(ui->loaderRefreshBtn, &QPushButton::clicked, this, &VanillaPage::loaderRefresh); + } void VanillaPage::openedImpl() @@ -80,6 +85,13 @@ void VanillaPage::refresh() ui->versionList->loadList(); } +void VanillaPage::loaderRefresh() +{ + if(ui->noneFilter->isChecked()) + return; + ui->loaderVersionList->loadList(); +} + void VanillaPage::filterChanged() { QStringList out; @@ -99,6 +111,38 @@ void VanillaPage::filterChanged() ui->versionList->setFilter(BaseVersionList::TypeRole, new RegexpFilter(regexp, false)); } +void VanillaPage::loaderFilterChanged() +{ + if(ui->noneFilter->isChecked()) + { + ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // empty list + // TODO: The below message does not show when the add instance window is first opened. This should be fixed. + ui->loaderVersionList->setEmptyString(tr("No mod loader is selected.")); + return; + } + else if(ui->forgeFilter->isChecked()) + { + ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, m_selectedVersion->descriptor()); + m_selectedLoader = "net.minecraftforge"; + } + else if(ui->fabricFilter->isChecked()) + { + ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, ""); + m_selectedLoader = "net.fabricmc.fabric-loader"; + } + else if(ui->liteLoaderFilter->isChecked()) + { + ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, m_selectedVersion->descriptor()); + m_selectedLoader = "com.mumfrey.liteloader"; + } + + auto vlist = APPLICATION->metadataIndex()->get(m_selectedLoader); + ui->loaderVersionList->initialize(vlist.get()); + ui->loaderVersionList->selectRecommended(); + suggestCurrent(); + ui->loaderVersionList->setEmptyString(tr("No versions are currently available for Minecraft %1").arg(m_selectedVersion->descriptor())); +} + VanillaPage::~VanillaPage() { delete ui; @@ -119,6 +163,16 @@ BaseVersionPtr VanillaPage::selectedVersion() const return m_selectedVersion; } +BaseVersionPtr VanillaPage::selectedLoaderVersion() const +{ + return m_selectedLoaderVersion; +} + +QString VanillaPage::selectedLoader() const +{ + return m_selectedLoader; +} + void VanillaPage::suggestCurrent() { if (!isOpened) @@ -132,7 +186,15 @@ void VanillaPage::suggestCurrent() return; } - dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion)); + // List is empty if either no mod loader is selected, or no versions are available + if(!ui->loaderVersionList->hasVersions()) + dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion)); + else + { + dialog->setSuggestedPack(m_selectedVersion->descriptor(), + new InstanceCreationTask(m_selectedVersion, m_selectedLoader, + m_selectedLoaderVersion)); + } dialog->setSuggestedIcon("default"); } @@ -140,4 +202,11 @@ void VanillaPage::setSelectedVersion(BaseVersionPtr version) { m_selectedVersion = version; suggestCurrent(); + loaderFilterChanged(); +} + +void VanillaPage::setSelectedLoaderVersion(BaseVersionPtr version) +{ + m_selectedLoaderVersion = version; + suggestCurrent(); } diff --git a/launcher/ui/pages/modplatform/VanillaPage.h b/launcher/ui/pages/modplatform/VanillaPage.h index 4e7479df..7193597d 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.h +++ b/launcher/ui/pages/modplatform/VanillaPage.h @@ -77,15 +77,20 @@ public: void openedImpl() override; BaseVersionPtr selectedVersion() const; + BaseVersionPtr selectedLoaderVersion() const; + QString selectedLoader() const; public slots: void setSelectedVersion(BaseVersionPtr version); + void setSelectedLoaderVersion(BaseVersionPtr version); private slots: void filterChanged(); + void loaderFilterChanged(); private: void refresh(); + void loaderRefresh(); void suggestCurrent(); private: @@ -94,4 +99,6 @@ private: Ui::VanillaPage *ui = nullptr; bool m_versionSetByUser = false; BaseVersionPtr m_selectedVersion; + BaseVersionPtr m_selectedLoaderVersion; + QString m_selectedLoader; }; diff --git a/launcher/ui/pages/modplatform/VanillaPage.ui b/launcher/ui/pages/modplatform/VanillaPage.ui index 870ff161..97724ea8 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.ui +++ b/launcher/ui/pages/modplatform/VanillaPage.ui @@ -33,113 +33,221 @@ - - - - - - Filter - - - Qt::AlignCenter - - - - - - - Releases - - - true - - - true - - - - - - - Snapshots - - - true - - - - - - - Old Snapshots - - - true - - - - - - - Betas - - - true - - - - - - - Alphas - - - true - - - - - - - Experiments - - - true - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Refresh - - - - - - - + + - + 0 0 + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + + + + + + + Filter + + + Qt::AlignCenter + + + + + + + Releases + + + true + + + true + + + + + + + Snapshots + + + true + + + + + + + Old Snapshots + + + true + + + + + + + Betas + + + true + + + + + + + Alphas + + + true + + + + + + + Experiments + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Refresh + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + Mod Loader + + + Qt::AlignCenter + + + + + + + None + + + true + + + loaderBtnGroup + + + + + + + Forge + + + loaderBtnGroup + + + + + + + Fabric + + + loaderBtnGroup + + + + + + + LiteLoader + + + loaderBtnGroup + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Refresh + + + + + + + @@ -166,4 +274,7 @@ + + + From 7aeccbb6b0e5f0af01c98d4dc5eb322888a45900 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Tue, 5 Apr 2022 13:47:27 -0400 Subject: [PATCH 248/605] Fix build on Qt 5.6 --- launcher/ui/pages/modplatform/VanillaPage.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/VanillaPage.cpp index 959e695c..a99f107e 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.cpp +++ b/launcher/ui/pages/modplatform/VanillaPage.cpp @@ -61,7 +61,10 @@ VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent) connect(ui->refreshBtn, &QPushButton::clicked, this, &VanillaPage::refresh); connect(ui->loaderVersionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedLoaderVersion); - connect(ui->loaderBtnGroup, &QButtonGroup::idToggled, this, &VanillaPage::loaderFilterChanged); + connect(ui->noneFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); + connect(ui->forgeFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); + connect(ui->fabricFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); + connect(ui->liteLoaderFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); connect(ui->loaderRefreshBtn, &QPushButton::clicked, this, &VanillaPage::loaderRefresh); } From 2cb242e9b3174137b5ff97d3781ae253e8208843 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Tue, 5 Apr 2022 20:27:31 -0400 Subject: [PATCH 249/605] Show no loader selected message when add instance window first opens This resolves an issue where the message only shows when selecting a mod loader and then selecting "None" again. --- launcher/ui/pages/modplatform/VanillaPage.cpp | 1 + launcher/ui/widgets/VersionSelectWidget.cpp | 6 +++++- launcher/ui/widgets/VersionSelectWidget.h | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/VanillaPage.cpp index a99f107e..64015eb7 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.cpp +++ b/launcher/ui/pages/modplatform/VanillaPage.cpp @@ -121,6 +121,7 @@ void VanillaPage::loaderFilterChanged() ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // empty list // TODO: The below message does not show when the add instance window is first opened. This should be fixed. ui->loaderVersionList->setEmptyString(tr("No mod loader is selected.")); + ui->loaderVersionList->setEmptyMode(VersionListView::String); return; } else if(ui->forgeFilter->isChecked()) diff --git a/launcher/ui/widgets/VersionSelectWidget.cpp b/launcher/ui/widgets/VersionSelectWidget.cpp index 1209f118..cc4fc6a2 100644 --- a/launcher/ui/widgets/VersionSelectWidget.cpp +++ b/launcher/ui/widgets/VersionSelectWidget.cpp @@ -4,7 +4,6 @@ #include #include -#include "VersionListView.h" #include "VersionProxyModel.h" #include "ui/dialogs/CustomMessageBox.h" @@ -57,6 +56,11 @@ void VersionSelectWidget::setEmptyErrorString(QString emptyErrorString) listView->setEmptyErrorString(emptyErrorString); } +void VersionSelectWidget::setEmptyMode(VersionListView::EmptyMode mode) +{ + listView->setEmptyMode(mode); +} + VersionSelectWidget::~VersionSelectWidget() { } diff --git a/launcher/ui/widgets/VersionSelectWidget.h b/launcher/ui/widgets/VersionSelectWidget.h index 0a649408..f56daa8a 100644 --- a/launcher/ui/widgets/VersionSelectWidget.h +++ b/launcher/ui/widgets/VersionSelectWidget.h @@ -18,6 +18,7 @@ #include #include #include "BaseVersionList.h" +#include "VersionListView.h" class VersionProxyModel; class VersionListView; @@ -49,6 +50,7 @@ public: void setFilter(BaseVersionList::ModelRoles role, Filter *filter); void setEmptyString(QString emptyString); void setEmptyErrorString(QString emptyErrorString); + void setEmptyMode(VersionListView::EmptyMode mode); void setResizeOn(int column); signals: From 7577115c3ca30b50268ba1963c72b57dc3a29db9 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Wed, 6 Apr 2022 16:58:57 -0400 Subject: [PATCH 250/605] Fix Fabric versions appearing for unsupported MC versions Also remove an old TODO comment, mentioning an issue that was already fixed. --- launcher/ui/pages/modplatform/VanillaPage.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/VanillaPage.cpp index 64015eb7..74c1bed4 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.cpp +++ b/launcher/ui/pages/modplatform/VanillaPage.cpp @@ -44,6 +44,7 @@ #include "ui/dialogs/NewInstanceDialog.h" #include "Filter.h" #include "InstanceCreationTask.h" +#include "Version.h" VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent) : QWidget(parent), dialog(dialog), ui(new Ui::VanillaPage) @@ -116,27 +117,31 @@ void VanillaPage::filterChanged() void VanillaPage::loaderFilterChanged() { + auto minecraftVersion = m_selectedVersion->descriptor(); if(ui->noneFilter->isChecked()) { ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // empty list - // TODO: The below message does not show when the add instance window is first opened. This should be fixed. ui->loaderVersionList->setEmptyString(tr("No mod loader is selected.")); ui->loaderVersionList->setEmptyMode(VersionListView::String); return; } else if(ui->forgeFilter->isChecked()) { - ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, m_selectedVersion->descriptor()); + ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion); m_selectedLoader = "net.minecraftforge"; } else if(ui->fabricFilter->isChecked()) { - ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, ""); + // FIXME: dirty hack because the launcher is unaware of Fabric's dependencies + if (Version(minecraftVersion) >= Version("1.14")) // Fabric supported + ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, ""); + else // Fabric unsupported + ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // clear list m_selectedLoader = "net.fabricmc.fabric-loader"; } else if(ui->liteLoaderFilter->isChecked()) { - ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, m_selectedVersion->descriptor()); + ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion); m_selectedLoader = "com.mumfrey.liteloader"; } @@ -144,7 +149,7 @@ void VanillaPage::loaderFilterChanged() ui->loaderVersionList->initialize(vlist.get()); ui->loaderVersionList->selectRecommended(); suggestCurrent(); - ui->loaderVersionList->setEmptyString(tr("No versions are currently available for Minecraft %1").arg(m_selectedVersion->descriptor())); + ui->loaderVersionList->setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion)); } VanillaPage::~VanillaPage() From 3e64935844f5ad1e772eab763a7f02260c25dcc1 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 15 Apr 2022 15:04:49 -0400 Subject: [PATCH 251/605] Add Quilt install option while creating an instance --- launcher/ui/pages/modplatform/VanillaPage.cpp | 14 ++++++++++++-- launcher/ui/pages/modplatform/VanillaPage.ui | 10 ++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/VanillaPage.cpp index 74c1bed4..207d0130 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.cpp +++ b/launcher/ui/pages/modplatform/VanillaPage.cpp @@ -65,6 +65,7 @@ VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent) connect(ui->noneFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); connect(ui->forgeFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); connect(ui->fabricFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); + connect(ui->quiltFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); connect(ui->liteLoaderFilter, &QRadioButton::toggled, this, &VanillaPage::loaderFilterChanged); connect(ui->loaderRefreshBtn, &QPushButton::clicked, this, &VanillaPage::loaderRefresh); @@ -133,12 +134,21 @@ void VanillaPage::loaderFilterChanged() else if(ui->fabricFilter->isChecked()) { // FIXME: dirty hack because the launcher is unaware of Fabric's dependencies - if (Version(minecraftVersion) >= Version("1.14")) // Fabric supported + if (Version(minecraftVersion) >= Version("1.14")) // Fabric/Quilt supported ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, ""); - else // Fabric unsupported + else // Fabric/Quilt unsupported ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // clear list m_selectedLoader = "net.fabricmc.fabric-loader"; } + else if(ui->quiltFilter->isChecked()) + { + // FIXME: dirty hack because the launcher is unaware of Quilt's dependencies (same as Fabric) + if (Version(minecraftVersion) >= Version("1.14")) // Fabric/Quilt supported + ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, ""); + else // Fabric/Quilt unsupported + ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // clear list + m_selectedLoader = "org.quiltmc.quilt-loader"; + } else if(ui->liteLoaderFilter->isChecked()) { ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion); diff --git a/launcher/ui/pages/modplatform/VanillaPage.ui b/launcher/ui/pages/modplatform/VanillaPage.ui index 97724ea8..43110927 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.ui +++ b/launcher/ui/pages/modplatform/VanillaPage.ui @@ -214,6 +214,16 @@ + + + + Quilt + + + loaderBtnGroup + + + From 5d8d7740ba85d1244948ccfadfa273bfa51d7c02 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 15 Apr 2022 15:55:03 -0400 Subject: [PATCH 252/605] Only enable instance options while an instance is selected --- launcher/ui/MainWindow.cpp | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 895b9881..97610e17 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -538,65 +538,79 @@ public: openAct = new QAction(tr("&Launch"), MainWindow); openAct->setShortcuts(QKeySequence::Open); openAct->setStatusTip(tr("Launch the selected instance")); + openAct->setEnabled(false); connect(openAct, &QAction::triggered, MainWindow, &MainWindow::on_actionLaunchInstance_triggered); - openOfflineAct = new QAction(tr("&Launch Offline"), MainWindow); + openOfflineAct = new QAction(tr("Launch &Offline"), MainWindow); openOfflineAct->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); openOfflineAct->setStatusTip(tr("Launch the selected instance in offline mode")); + openOfflineAct->setEnabled(false); connect(openOfflineAct, &QAction::triggered, MainWindow, &MainWindow::on_actionLaunchInstanceOffline_triggered); editInstanceAct = new QAction(tr("&Edit Instance..."), MainWindow); editInstanceAct->setShortcut(QKeySequence(tr("Ctrl+I"))); editInstanceAct->setStatusTip(tr("Edit the selected instance")); + editInstanceAct->setEnabled(false); connect(editInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionEditInstance_triggered); editNotesAct = new QAction(tr("&Edit Notes..."), MainWindow); editNotesAct->setStatusTip(tr("Edit the selected instance's notes")); + editNotesAct->setEnabled(false); connect(editNotesAct, &QAction::triggered, MainWindow, &MainWindow::on_actionEditInstNotes_triggered); editModsAct = new QAction(tr("&View Mods"), MainWindow); editModsAct->setStatusTip(tr("View the selected instance's mods")); + editModsAct->setEnabled(false); connect(editModsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionMods_triggered); editWorldsAct = new QAction(tr("&View Worlds"), MainWindow); editWorldsAct->setStatusTip(tr("View the selected instance's worlds")); + editWorldsAct->setEnabled(false); connect(editWorldsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionWorlds_triggered); manageScreenshotsAct = new QAction(tr("&Manage Screenshots"), MainWindow); manageScreenshotsAct->setStatusTip(tr("Manage the selected instance's screenshots")); + manageScreenshotsAct->setEnabled(false); connect(manageScreenshotsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionScreenshots_triggered); changeGroupAct = new QAction(tr("&Change Group..."), MainWindow); changeGroupAct->setShortcut(QKeySequence(tr("Ctrl+G"))); changeGroupAct->setStatusTip(tr("Change the selected instance's group")); + changeGroupAct->setEnabled(false); connect(changeGroupAct, &QAction::triggered, MainWindow, &MainWindow::on_actionChangeInstGroup_triggered); openMCFolderAct = new QAction(tr("&Open Minecraft Folder"), MainWindow); openMCFolderAct->setShortcut(QKeySequence(tr("Ctrl+M"))); openMCFolderAct->setStatusTip(tr("Open the selected instance's Minecraft folder")); + openMCFolderAct->setEnabled(false); connect(openMCFolderAct, &QAction::triggered, MainWindow, &MainWindow::on_actionViewSelectedMCFolder_triggered); openConfigFolderAct = new QAction(tr("&Open Config Folder"), MainWindow); openConfigFolderAct->setStatusTip(tr("Open the selected instance's config folder")); + openConfigFolderAct->setEnabled(false); connect(openConfigFolderAct, &QAction::triggered, MainWindow, &MainWindow::on_actionConfig_Folder_triggered); openInstanceFolderAct = new QAction(tr("&Open Instance Folder"), MainWindow); openInstanceFolderAct->setStatusTip(tr("Open the selected instance's main folder")); + openInstanceFolderAct->setEnabled(false); connect(openInstanceFolderAct, &QAction::triggered, MainWindow, &MainWindow::on_actionViewInstanceFolder_triggered); exportInstanceAct = new QAction(tr("&Export Instance..."), MainWindow); exportInstanceAct->setShortcut(QKeySequence(tr("Ctrl+E"))); exportInstanceAct->setStatusTip(tr("Export the selected instance")); + exportInstanceAct->setEnabled(false); connect(exportInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionExportInstance_triggered); deleteInstanceAct = new QAction(tr("&Delete Instance..."), MainWindow); deleteInstanceAct->setShortcut(QKeySequence::Delete); deleteInstanceAct->setStatusTip(tr("Delete the selected instance")); + deleteInstanceAct->setEnabled(false); connect(deleteInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionDeleteInstance_triggered); duplicateInstanceAct = new QAction(tr("&Copy Instance..."), MainWindow); duplicateInstanceAct->setShortcut(QKeySequence(tr("Ctrl+D"))); duplicateInstanceAct->setStatusTip(tr("Duplicate the selected instance")); + duplicateInstanceAct->setEnabled(false); connect(duplicateInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionCopyInstance_triggered); closeAct = new QAction(tr("&Close Window"), MainWindow); @@ -673,6 +687,25 @@ public: connect(redditAct, &QAction::triggered, MainWindow, &MainWindow::on_actionREDDIT_triggered); } + // "Instance actions" are actions that require an instance to be selected (i.e. "new instance" is not here) + void setInstanceActionsEnabled(bool enabled) const + { + openAct->setEnabled(enabled); + openOfflineAct->setEnabled(enabled); + editInstanceAct->setEnabled(enabled); + editNotesAct->setEnabled(enabled); + editModsAct->setEnabled(enabled); + editWorldsAct->setEnabled(enabled); + manageScreenshotsAct->setEnabled(enabled); + changeGroupAct->setEnabled(enabled); + openMCFolderAct->setEnabled(enabled); + openConfigFolderAct->setEnabled(enabled); + openInstanceFolderAct->setEnabled(enabled); + exportInstanceAct->setEnabled(enabled); + deleteInstanceAct->setEnabled(enabled); + duplicateInstanceAct->setEnabled(enabled); + } + void createStatusBar(QMainWindow *MainWindow) { statusBar = new QStatusBar(MainWindow); @@ -2171,6 +2204,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & if (m_selectedInstance) { ui->instanceToolBar->setEnabled(true); + ui->setInstanceActionsEnabled(true); if(m_selectedInstance->isRunning()) { ui->actionLaunchInstance->setEnabled(true); @@ -2195,6 +2229,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & else { ui->instanceToolBar->setEnabled(false); + ui->setInstanceActionsEnabled(false); APPLICATION->settings()->set("SelectedInstance", QString()); selectionBad(); return; @@ -2223,6 +2258,7 @@ void MainWindow::selectionBad() statusBar()->clearMessage(); ui->instanceToolBar->setEnabled(false); + ui->setInstanceActionsEnabled(false); ui->renameButton->setText(tr("Rename Instance")); updateInstanceToolIcon("grass"); From b0a8bd7dfe3c0d855da0d6d6aea45dc7151ec08c Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 15 Apr 2022 16:29:29 -0400 Subject: [PATCH 253/605] Improve menu bar keyboard usability More reasonable (unique) menu access keys were chosen. In addition, move the settings action from the Help menu to the Edit menu. --- launcher/ui/MainWindow.cpp | 44 +++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 97610e17..6f638f74 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -508,6 +508,7 @@ public: editMenu->addAction(pasteAct); editMenu->addAction(selectAllAct); editMenu->addSeparator(); + editMenu->addAction(settingsAct); profileMenu = menuBar->addMenu(tr("&Profiles")); // TODO: add a list of logged in accounts here @@ -515,7 +516,6 @@ public: helpMenu = menuBar->addMenu(tr("&Help")); helpMenu->addAction(aboutAct); - helpMenu->addAction(settingsAct); helpMenu->addAction(wikiAct); helpMenu->addAction(newsAct); helpMenu->addSeparator(); @@ -547,18 +547,18 @@ public: openOfflineAct->setEnabled(false); connect(openOfflineAct, &QAction::triggered, MainWindow, &MainWindow::on_actionLaunchInstanceOffline_triggered); - editInstanceAct = new QAction(tr("&Edit Instance..."), MainWindow); + editInstanceAct = new QAction(tr("Edit Inst&ance..."), MainWindow); editInstanceAct->setShortcut(QKeySequence(tr("Ctrl+I"))); editInstanceAct->setStatusTip(tr("Edit the selected instance")); editInstanceAct->setEnabled(false); connect(editInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionEditInstance_triggered); - editNotesAct = new QAction(tr("&Edit Notes..."), MainWindow); + editNotesAct = new QAction(tr("E&dit Notes..."), MainWindow); editNotesAct->setStatusTip(tr("Edit the selected instance's notes")); editNotesAct->setEnabled(false); connect(editNotesAct, &QAction::triggered, MainWindow, &MainWindow::on_actionEditInstNotes_triggered); - editModsAct = new QAction(tr("&View Mods"), MainWindow); + editModsAct = new QAction(tr("View &Mods"), MainWindow); editModsAct->setStatusTip(tr("View the selected instance's mods")); editModsAct->setEnabled(false); connect(editModsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionMods_triggered); @@ -568,7 +568,7 @@ public: editWorldsAct->setEnabled(false); connect(editWorldsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionWorlds_triggered); - manageScreenshotsAct = new QAction(tr("&Manage Screenshots"), MainWindow); + manageScreenshotsAct = new QAction(tr("Manage &Screenshots"), MainWindow); manageScreenshotsAct->setStatusTip(tr("Manage the selected instance's screenshots")); manageScreenshotsAct->setEnabled(false); connect(manageScreenshotsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionScreenshots_triggered); @@ -579,41 +579,41 @@ public: changeGroupAct->setEnabled(false); connect(changeGroupAct, &QAction::triggered, MainWindow, &MainWindow::on_actionChangeInstGroup_triggered); - openMCFolderAct = new QAction(tr("&Open Minecraft Folder"), MainWindow); + openMCFolderAct = new QAction(tr("Open Minec&raft Folder"), MainWindow); openMCFolderAct->setShortcut(QKeySequence(tr("Ctrl+M"))); openMCFolderAct->setStatusTip(tr("Open the selected instance's Minecraft folder")); openMCFolderAct->setEnabled(false); connect(openMCFolderAct, &QAction::triggered, MainWindow, &MainWindow::on_actionViewSelectedMCFolder_triggered); - openConfigFolderAct = new QAction(tr("&Open Config Folder"), MainWindow); + openConfigFolderAct = new QAction(tr("&Open Confi&g Folder"), MainWindow); openConfigFolderAct->setStatusTip(tr("Open the selected instance's config folder")); openConfigFolderAct->setEnabled(false); connect(openConfigFolderAct, &QAction::triggered, MainWindow, &MainWindow::on_actionConfig_Folder_triggered); - openInstanceFolderAct = new QAction(tr("&Open Instance Folder"), MainWindow); + openInstanceFolderAct = new QAction(tr("&Open &Instance Folder"), MainWindow); openInstanceFolderAct->setStatusTip(tr("Open the selected instance's main folder")); openInstanceFolderAct->setEnabled(false); connect(openInstanceFolderAct, &QAction::triggered, MainWindow, &MainWindow::on_actionViewInstanceFolder_triggered); - exportInstanceAct = new QAction(tr("&Export Instance..."), MainWindow); + exportInstanceAct = new QAction(tr("E&xport Instance..."), MainWindow); exportInstanceAct->setShortcut(QKeySequence(tr("Ctrl+E"))); exportInstanceAct->setStatusTip(tr("Export the selected instance")); exportInstanceAct->setEnabled(false); connect(exportInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionExportInstance_triggered); - deleteInstanceAct = new QAction(tr("&Delete Instance..."), MainWindow); + deleteInstanceAct = new QAction(tr("Dele&te Instance..."), MainWindow); deleteInstanceAct->setShortcut(QKeySequence::Delete); deleteInstanceAct->setStatusTip(tr("Delete the selected instance")); deleteInstanceAct->setEnabled(false); connect(deleteInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionDeleteInstance_triggered); - duplicateInstanceAct = new QAction(tr("&Copy Instance..."), MainWindow); + duplicateInstanceAct = new QAction(tr("Cop&y Instance..."), MainWindow); duplicateInstanceAct->setShortcut(QKeySequence(tr("Ctrl+D"))); duplicateInstanceAct->setStatusTip(tr("Duplicate the selected instance")); duplicateInstanceAct->setEnabled(false); connect(duplicateInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionCopyInstance_triggered); - closeAct = new QAction(tr("&Close Window"), MainWindow); + closeAct = new QAction(tr("Close &Window"), MainWindow); closeAct->setShortcut(QKeySequence::Close); closeAct->setStatusTip(tr("Close the current window")); // FIXME: currently this always closes the main window, even if it is not currently the window in focus @@ -629,7 +629,7 @@ public: redoAct->setStatusTip(tr("Redo")); redoAct->setEnabled(false); - cutAct = new QAction(tr("&Cut"), MainWindow); + cutAct = new QAction(tr("Cu&t"), MainWindow); cutAct->setShortcuts(QKeySequence::Cut); cutAct->setStatusTip(tr("Cut")); cutAct->setEnabled(false); @@ -644,11 +644,16 @@ public: pasteAct->setStatusTip(tr("Paste")); pasteAct->setEnabled(false); - selectAllAct = new QAction(tr("&Select All"), MainWindow); + selectAllAct = new QAction(tr("Select &All"), MainWindow); selectAllAct->setShortcuts(QKeySequence::SelectAll); selectAllAct->setStatusTip(tr("Select all")); selectAllAct->setEnabled(false); + settingsAct = new QAction(tr("&Settings..."), MainWindow); + settingsAct->setShortcut(QKeySequence::Preferences); + settingsAct->setStatusTip(tr("Change %1 settings").arg(BuildConfig.LAUNCHER_NAME)); + connect(settingsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionSettings_triggered); + manageAccountAct = new QAction(tr("&Manage Accounts..."), MainWindow); manageAccountAct->setStatusTip(tr("Open account manager")); connect(manageAccountAct, &QAction::triggered, MainWindow, &MainWindow::on_actionManageAccounts_triggered); @@ -657,20 +662,15 @@ public: aboutAct->setStatusTip(tr("About %1").arg(BuildConfig.LAUNCHER_NAME)); connect(aboutAct, &QAction::triggered, MainWindow, &MainWindow::on_actionAbout_triggered); - settingsAct = new QAction(tr("&Settings..."), MainWindow); - settingsAct->setShortcut(QKeySequence::Preferences); - settingsAct->setStatusTip(tr("Change %1 settings").arg(BuildConfig.LAUNCHER_NAME)); - connect(settingsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionSettings_triggered); - - wikiAct = new QAction(tr("&%1 Help").arg(BuildConfig.LAUNCHER_NAME), MainWindow); + wikiAct = new QAction(tr("%1 He&lp").arg(BuildConfig.LAUNCHER_NAME), MainWindow); wikiAct->setStatusTip(tr("Open %1's wiki").arg(BuildConfig.LAUNCHER_NAME)); connect(wikiAct, &QAction::triggered, MainWindow, &MainWindow::on_actionOpenWiki_triggered); - newsAct = new QAction(tr("&%1 News").arg(BuildConfig.LAUNCHER_NAME), MainWindow); + newsAct = new QAction(tr("&%1 &News").arg(BuildConfig.LAUNCHER_NAME), MainWindow); newsAct->setStatusTip(tr("Open %1's news").arg(BuildConfig.LAUNCHER_NAME)); connect(newsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionMoreNews_triggered); - reportBugAct = new QAction(tr("&Report Bugs..."), MainWindow); + reportBugAct = new QAction(tr("Report &Bugs..."), MainWindow); reportBugAct->setStatusTip(tr("Report bugs to the developers")); connect(reportBugAct, &QAction::triggered, MainWindow, &MainWindow::on_actionReportBug_triggered); From 80ec178d5f1fbb10657ea91629381cde55bf9efb Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 15 Apr 2022 16:38:26 -0400 Subject: [PATCH 254/605] Fix keyboard shortcut for delete instance on some devices My laptop has a key labeled "delete," but for some reason it doesn't work with `QKeySequence::Delete`. Instead it's interpreted as a backspace. --- launcher/ui/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 6f638f74..443b0c54 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -602,7 +602,7 @@ public: connect(exportInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionExportInstance_triggered); deleteInstanceAct = new QAction(tr("Dele&te Instance..."), MainWindow); - deleteInstanceAct->setShortcut(QKeySequence::Delete); + deleteInstanceAct->setShortcuts({QKeySequence(tr("Backspace")), QKeySequence::Delete}); deleteInstanceAct->setStatusTip(tr("Delete the selected instance")); deleteInstanceAct->setEnabled(false); connect(deleteInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionDeleteInstance_triggered); From f6605bc3f82df4a3f190bb9e1d935de295329b54 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 15 Apr 2022 16:44:27 -0400 Subject: [PATCH 255/605] Implement help (open wiki) menu bar action --- launcher/ui/MainWindow.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 443b0c54..a51aea8a 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -2013,8 +2013,7 @@ void MainWindow::on_actionReportBug_triggered() void MainWindow::on_actionOpenWiki_triggered() { - // TODO: add functionality -// DesktopServices::openUrl(QUrl(BuildConfig.WIKI_URL)); + DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg(""))); } void MainWindow::on_actionMoreNews_triggered() From 1303771b58a213a25faaa1a870c774fbb72e7513 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 15 Apr 2022 18:25:37 -0400 Subject: [PATCH 256/605] Add option to always show menu bar instead of toolbar For those who like keyboard navigation at the expense of aesthetics. --- launcher/Application.cpp | 2 ++ launcher/ui/MainWindow.cpp | 17 ++++++++---- launcher/ui/MainWindow.h | 2 ++ launcher/ui/pages/global/LauncherPage.cpp | 7 +++++ launcher/ui/pages/global/LauncherPage.ui | 34 +++++++++++++++++++---- 5 files changed, 51 insertions(+), 11 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 690a7ee4..25ac93e6 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -613,6 +613,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Remembered state m_settings->registerSetting("LastUsedGroupForNewInstance", QString()); + m_settings->registerSetting("MenuBarInsteadOfToolBar", false); + QString defaultMonospace; int defaultSize = 11; #ifdef Q_OS_WIN32 diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index a51aea8a..c144231d 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -309,6 +309,7 @@ public: void createMainToolbar(QMainWindow *MainWindow) { mainToolBar = TranslatedToolbar(MainWindow); + mainToolBar->setVisible(menuBar->isNativeMenuBar() || !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); mainToolBar->setObjectName(QStringLiteral("mainToolBar")); mainToolBar->setMovable(true); mainToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); @@ -471,8 +472,7 @@ public: void createMenuBar(MainWindow *MainWindow) { menuBar = new QMenuBar(MainWindow); - // There's already a toolbar, so hide this menu bar by default unless 'alt' is pressed on systems without native menu bar - menuBar->setVisible(false); + menuBar->setVisible(APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); createMenuActions(MainWindow); // TODO: only enable options while an instance is selected (if applicable) @@ -909,10 +909,10 @@ public: MainWindow->setAccessibleName(BuildConfig.LAUNCHER_NAME); #endif - createMainToolbar(MainWindow); - createMenuBar(dynamic_cast(MainWindow)); + createMainToolbar(MainWindow); + centralWidget = new QWidget(MainWindow); centralWidget->setObjectName(QStringLiteral("centralWidget")); horizontalLayout = new QHBoxLayout(centralWidget); @@ -1135,7 +1135,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow #ifndef Q_OS_MAC void MainWindow::keyReleaseEvent(QKeyEvent *event) { - if(event->key()==Qt::Key_Alt) + if(event->key()==Qt::Key_Alt && !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()) ui->menuBar->setVisible(!ui->menuBar->isVisible()); else QMainWindow::keyReleaseEvent(event); @@ -1294,6 +1294,12 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos) myMenu.exec(view->mapToGlobal(pos)); } +void MainWindow::updateMainToolBar() +{ + ui->menuBar->setVisible(APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); + ui->mainToolBar->setVisible(ui->menuBar->isNativeMenuBar() || !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); +} + void MainWindow::updateToolsMenu() { QToolButton *launchButton = dynamic_cast(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstance)); @@ -1966,6 +1972,7 @@ void MainWindow::globalSettingsClosed() APPLICATION->instances()->loadList(); proxymodel->invalidate(); proxymodel->sort(0); + updateMainToolBar(); updateToolsMenu(); updateStatusCenter(); update(); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index c38ee073..5424a4a9 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -151,6 +151,8 @@ private slots: void showInstanceContextMenu(const QPoint &); + void updateMainToolBar(); + void updateToolsMenu(); void instanceActivated(QModelIndex); diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 42ad5ae3..a213eff0 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include "updater/UpdateChecker.h" @@ -322,6 +323,8 @@ void LauncherPage::applySettings() APPLICATION->setApplicationTheme(newAppTheme, false); } + s->set("MenuBarInsteadOfToolBar", ui->preferMenuBarCheckBox->isChecked()); + // Console settings s->set("ShowConsole", ui->showConsoleCheck->isChecked()); s->set("AutoCloseConsole", ui->autoCloseConsoleCheck->isChecked()); @@ -410,6 +413,10 @@ void LauncherPage::loadSettings() } } + // Toolbar/menu bar settings (not applicable if native menu bar is present) + ui->toolsBox->setVisible(!QMenuBar().isNativeMenuBar()); + ui->preferMenuBarCheckBox->setChecked(s->get("MenuBarInsteadOfToolBar").toBool()); + // Console settings ui->showConsoleCheck->setChecked(s->get("ShowConsole").toBool()); ui->autoCloseConsoleCheck->setChecked(s->get("AutoCloseConsole").toBool()); diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index c110dd09..636aec15 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -290,6 +290,16 @@ + + + + Colors + + + themeComboBoxColors + + + @@ -303,13 +313,25 @@ - - + + + + + + + + 0 + 0 + + + + Tools + + + + - Colors - - - themeComboBoxColors + Always show menu bar instead of tool bar (more keyboard friendly, less pretty) From 1049507b3fe1b103e3089bb1178cac3bc92ff964 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 15 Apr 2022 19:55:49 -0400 Subject: [PATCH 257/605] Add logged in accounts to the profiles menu bar menu Additionally, add keyboard shortcuts for switching between different accounts. --- launcher/ui/MainWindow.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index c144231d..90af763a 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -511,7 +511,6 @@ public: editMenu->addAction(settingsAct); profileMenu = menuBar->addMenu(tr("&Profiles")); - // TODO: add a list of logged in accounts here profileMenu->addAction(manageAccountAct); helpMenu = menuBar->addMenu(tr("&Help")); @@ -1379,6 +1378,7 @@ void MainWindow::updateToolsMenu() void MainWindow::repopulateAccountsMenu() { accountMenu->clear(); + ui->profileMenu->clear(); auto accounts = APPLICATION->accounts(); MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); @@ -1399,6 +1399,7 @@ void MainWindow::repopulateAccountsMenu() QAction *action = new QAction(tr("No accounts added!"), this); action->setEnabled(false); accountMenu->addAction(action); + ui->profileMenu->addAction(action); } else { @@ -1422,26 +1423,39 @@ void MainWindow::repopulateAccountsMenu() else { action->setIcon(APPLICATION->getThemedIcon("noaccount")); } + + const int highestNumberKey = 9; + if(isetShortcut(QKeySequence(tr("Ctrl+%1").arg(i + 1))); + } + accountMenu->addAction(action); + ui->profileMenu->addAction(action); connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); } } accountMenu->addSeparator(); + ui->profileMenu->addSeparator(); QAction *action = new QAction(tr("No Default Account"), this); action->setCheckable(true); action->setIcon(APPLICATION->getThemedIcon("noaccount")); action->setData(-1); + action->setShortcut(QKeySequence(tr("Ctrl+0"))); if (!defaultAccount) { action->setChecked(true); } accountMenu->addAction(action); + ui->profileMenu->addAction(action); connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); accountMenu->addSeparator(); + ui->profileMenu->addSeparator(); accountMenu->addAction(ui->actionManageAccounts); + ui->profileMenu->addAction(ui->manageAccountAct); } void MainWindow::updatesAllowedChanged(bool allowed) From e59d3a339fcba0d17fd70df5690d358d1da315ac Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sat, 16 Apr 2022 02:07:29 -0400 Subject: [PATCH 258/605] Close the current window instead of the main window from the menu bar Systems with native menu bars show the same menu bar for all child windows. As a result, you cannot assume that the menu bar's parent (the `MainWindow`) will be the window in focus. --- launcher/Application.cpp | 7 +++++++ launcher/Application.h | 1 + launcher/ui/MainWindow.cpp | 3 +-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 25ac93e6..01b62971 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -80,6 +80,7 @@ #include #include #include +#include #include "InstanceList.h" @@ -1267,6 +1268,12 @@ bool Application::kill(InstancePtr instance) return true; } +void Application::closeCurrentWindow() +{ + if (focusWindow()) + focusWindow()->close(); +} + void Application::addRunningInstance() { m_runningInstances ++; diff --git a/launcher/Application.h b/launcher/Application.h index 54d9ba5f..172321c0 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -189,6 +189,7 @@ public slots: MinecraftAccountPtr accountToUse = nullptr ); bool kill(InstancePtr instance); + void closeCurrentWindow(); private slots: void on_windowClose(); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 90af763a..1c694484 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -615,8 +615,7 @@ public: closeAct = new QAction(tr("Close &Window"), MainWindow); closeAct->setShortcut(QKeySequence::Close); closeAct->setStatusTip(tr("Close the current window")); - // FIXME: currently this always closes the main window, even if it is not currently the window in focus - connect(closeAct, &QAction::triggered, MainWindow, &MainWindow::close); + connect(closeAct, &QAction::triggered, APPLICATION, &Application::closeCurrentWindow); undoAct = new QAction(tr("&Undo"), MainWindow); undoAct->setShortcuts(QKeySequence::Undo); From 6a97ac603abf5554e920caf74fff4b643cde85fe Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sat, 16 Apr 2022 03:32:08 -0400 Subject: [PATCH 259/605] Use preexisting actions in the menu bar The code is now much cleaner. Because the actions already present are enabled elsewhere even when the menu bar is hidden, keyboard shortcuts added to them automatically work regardless of whether the menu bar is visible. This means that the hacky workaround related to this can be removed. --- launcher/ui/MainWindow.cpp | 729 ++++++++++++++++--------------------- launcher/ui/MainWindow.h | 2 - 2 files changed, 307 insertions(+), 424 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 1c694484..27763387 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -247,38 +247,20 @@ public: QMenuBar *menuBar = nullptr; QMenu *fileMenu; QMenu *editMenu; + QMenu *viewMenu; QMenu *profileMenu; - QAction *newAct; - QAction *openAct; - QAction *openOfflineAct; - QAction *editInstanceAct; - QAction *editNotesAct; - QAction *editModsAct; - QAction *editWorldsAct; - QAction *manageScreenshotsAct; - QAction *changeGroupAct; - QAction *openMCFolderAct; - QAction *openConfigFolderAct; - QAction *openInstanceFolderAct; - QAction *exportInstanceAct; - QAction *deleteInstanceAct; - QAction *duplicateInstanceAct; - QAction *closeAct; - QAction *undoAct; - QAction *redoAct; - QAction *cutAct; - QAction *copyAct; - QAction *pasteAct; - QAction *selectAllAct; - QAction *manageAccountAct; - QAction *aboutAct; - QAction *settingsAct; - QAction *wikiAct; - QAction *newsAct; - QAction *reportBugAct; - QAction *matrixAct; - QAction *discordAct; - QAction *redditAct; + + QAction *actionCloseWindow; + + QAction *actionUndo; + QAction *actionRedo; + QAction *actionCut; + QAction *actionCopy; + QAction *actionPaste; + QAction *actionSelectAll; + + QAction *actionWiki; + QAction *actionNewsMenuBar; TranslatedToolbar mainToolBar; TranslatedToolbar instanceToolBar; @@ -290,12 +272,12 @@ public: { if(m_kill) { - actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Kill")); + actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Kill")); actionLaunchInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Kill the running instance")); } else { - actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch")); + actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Launch")); actionLaunchInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance.")); } actionLaunchInstance.retranslate(); @@ -306,6 +288,131 @@ public: updateLaunchAction(); } + void createMainToolbarActions(QMainWindow *MainWindow) + { + actionAddInstance = TranslatedAction(MainWindow); + actionAddInstance->setObjectName(QStringLiteral("actionAddInstance")); + actionAddInstance->setIcon(APPLICATION->getThemedIcon("new")); + actionAddInstance->setIconVisibleInMenu(false); + actionAddInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Add Instanc&e...")); + actionAddInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Add a new instance.")); + actionAddInstance->setShortcut(QKeySequence::New); + all_actions.append(&actionAddInstance); + + actionViewInstanceFolder = TranslatedAction(MainWindow); + actionViewInstanceFolder->setObjectName(QStringLiteral("actionViewInstanceFolder")); + actionViewInstanceFolder->setIcon(APPLICATION->getThemedIcon("viewfolder")); + actionViewInstanceFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Instance Folder")); + actionViewInstanceFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the instance folder in a file browser.")); + all_actions.append(&actionViewInstanceFolder); + + actionViewCentralModsFolder = TranslatedAction(MainWindow); + actionViewCentralModsFolder->setObjectName(QStringLiteral("actionViewCentralModsFolder")); + actionViewCentralModsFolder->setIcon(APPLICATION->getThemedIcon("centralmods")); + actionViewCentralModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Central Mods Folder")); + actionViewCentralModsFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the central mods folder in a file browser.")); + all_actions.append(&actionViewCentralModsFolder); + + foldersMenu = new QMenu(MainWindow); + foldersMenu->setTitle(tr("F&olders")); + foldersMenu->setToolTipsVisible(true); + + foldersMenu->addAction(actionViewInstanceFolder); + foldersMenu->addAction(actionViewCentralModsFolder); + + foldersMenuButton = TranslatedToolButton(MainWindow); + foldersMenuButton.setTextId(QT_TRANSLATE_NOOP("MainWindow", "F&olders")); + foldersMenuButton.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open one of the folders shared between instances.")); + foldersMenuButton->setMenu(foldersMenu); + foldersMenuButton->setPopupMode(QToolButton::InstantPopup); + foldersMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + foldersMenuButton->setIcon(APPLICATION->getThemedIcon("viewfolder")); + foldersMenuButton->setFocusPolicy(Qt::NoFocus); + all_toolbuttons.append(&foldersMenuButton); + + actionSettings = TranslatedAction(MainWindow); + actionSettings->setObjectName(QStringLiteral("actionSettings")); + actionSettings->setIcon(APPLICATION->getThemedIcon("settings")); + actionSettings->setMenuRole(QAction::PreferencesRole); + actionSettings.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Settings...")); + actionSettings.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change settings.")); + actionSettings->setShortcut(QKeySequence::Preferences); + all_actions.append(&actionSettings); + + if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { + actionReportBug = TranslatedAction(MainWindow); + actionReportBug->setObjectName(QStringLiteral("actionReportBug")); + actionReportBug->setIcon(APPLICATION->getThemedIcon("bug")); + actionReportBug.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Report a &Bug...")); + actionReportBug.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the bug tracker to report a bug with %1.")); + all_actions.append(&actionReportBug); + } + + if(!BuildConfig.MATRIX_URL.isEmpty()) { + actionMATRIX = TranslatedAction(MainWindow); + actionMATRIX->setObjectName(QStringLiteral("actionMATRIX")); + actionMATRIX->setIcon(APPLICATION->getThemedIcon("matrix")); + actionMATRIX.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Matrix Space")); + actionMATRIX.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Matrix space")); + all_actions.append(&actionMATRIX); + } + + if (!BuildConfig.DISCORD_URL.isEmpty()) { + actionDISCORD = TranslatedAction(MainWindow); + actionDISCORD->setObjectName(QStringLiteral("actionDISCORD")); + actionDISCORD->setIcon(APPLICATION->getThemedIcon("discord")); + actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Discord Guild")); + actionDISCORD.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Discord guild.")); + all_actions.append(&actionDISCORD); + } + + if (!BuildConfig.SUBREDDIT_URL.isEmpty()) { + actionREDDIT = TranslatedAction(MainWindow); + actionREDDIT->setObjectName(QStringLiteral("actionREDDIT")); + actionREDDIT->setIcon(APPLICATION->getThemedIcon("reddit-alien")); + actionREDDIT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Sub&reddit")); + actionREDDIT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 subreddit.")); + all_actions.append(&actionREDDIT); + } + + actionAbout = TranslatedAction(MainWindow); + actionAbout->setObjectName(QStringLiteral("actionAbout")); + actionAbout->setIcon(APPLICATION->getThemedIcon("about")); + actionAbout->setMenuRole(QAction::AboutRole); + actionAbout.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&About %1")); + actionAbout.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View information about %1.")); + all_actions.append(&actionAbout); + + if(BuildConfig.UPDATER_ENABLED) + { + actionCheckUpdate = TranslatedAction(MainWindow); + actionCheckUpdate->setObjectName(QStringLiteral("actionCheckUpdate")); + actionCheckUpdate->setIcon(APPLICATION->getThemedIcon("checkupdate")); + actionCheckUpdate.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Update...")); + actionCheckUpdate.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Check for new updates for %1.")); + actionCheckUpdate->setMenuRole(QAction::ApplicationSpecificRole); + all_actions.append(&actionCheckUpdate); + } + + actionCAT = TranslatedAction(MainWindow); + actionCAT->setObjectName(QStringLiteral("actionCAT")); + actionCAT->setCheckable(true); + actionCAT->setIcon(APPLICATION->getThemedIcon("cat")); + actionCAT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Meow")); + actionCAT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "It's a fluffy kitty :3")); + actionCAT->setPriority(QAction::LowPriority); + all_actions.append(&actionCAT); + + // profile menu and its actions + actionManageAccounts = TranslatedAction(MainWindow); + actionManageAccounts->setObjectName(QStringLiteral("actionManageAccounts")); + actionManageAccounts.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Manage Accounts...")); + // FIXME: no tooltip! + actionManageAccounts->setCheckable(false); + actionManageAccounts->setIcon(APPLICATION->getThemedIcon("accounts")); + all_actions.append(&actionManageAccounts); + } + void createMainToolbar(QMainWindow *MainWindow) { mainToolBar = TranslatedToolbar(MainWindow); @@ -317,107 +424,35 @@ public: mainToolBar->setFloatable(false); mainToolBar.setWindowTitleId(QT_TRANSLATE_NOOP("MainWindow", "Main Toolbar")); - actionAddInstance = TranslatedAction(MainWindow); - actionAddInstance->setObjectName(QStringLiteral("actionAddInstance")); - actionAddInstance->setIcon(APPLICATION->getThemedIcon("new")); - actionAddInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Add Instance")); - actionAddInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Add a new instance.")); - all_actions.append(&actionAddInstance); mainToolBar->addAction(actionAddInstance); mainToolBar->addSeparator(); - foldersMenu = new QMenu(MainWindow); - foldersMenu->setToolTipsVisible(true); - - actionViewInstanceFolder = TranslatedAction(MainWindow); - actionViewInstanceFolder->setObjectName(QStringLiteral("actionViewInstanceFolder")); - actionViewInstanceFolder->setIcon(APPLICATION->getThemedIcon("viewfolder")); - actionViewInstanceFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Instance Folder")); - actionViewInstanceFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the instance folder in a file browser.")); - all_actions.append(&actionViewInstanceFolder); - foldersMenu->addAction(actionViewInstanceFolder); - - actionViewCentralModsFolder = TranslatedAction(MainWindow); - actionViewCentralModsFolder->setObjectName(QStringLiteral("actionViewCentralModsFolder")); - actionViewCentralModsFolder->setIcon(APPLICATION->getThemedIcon("centralmods")); - actionViewCentralModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Central Mods Folder")); - actionViewCentralModsFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the central mods folder in a file browser.")); - all_actions.append(&actionViewCentralModsFolder); - foldersMenu->addAction(actionViewCentralModsFolder); - - foldersMenuButton = TranslatedToolButton(MainWindow); - foldersMenuButton.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Folders")); - foldersMenuButton.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open one of the folders shared between instances.")); - foldersMenuButton->setMenu(foldersMenu); - foldersMenuButton->setPopupMode(QToolButton::InstantPopup); - foldersMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - foldersMenuButton->setIcon(APPLICATION->getThemedIcon("viewfolder")); - foldersMenuButton->setFocusPolicy(Qt::NoFocus); - all_toolbuttons.append(&foldersMenuButton); QWidgetAction* foldersButtonAction = new QWidgetAction(MainWindow); foldersButtonAction->setDefaultWidget(foldersMenuButton); mainToolBar->addAction(foldersButtonAction); - actionSettings = TranslatedAction(MainWindow); - actionSettings->setObjectName(QStringLiteral("actionSettings")); - actionSettings->setIcon(APPLICATION->getThemedIcon("settings")); - actionSettings->setMenuRole(QAction::PreferencesRole); - actionSettings.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Settings")); - actionSettings.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change settings.")); - all_actions.append(&actionSettings); mainToolBar->addAction(actionSettings); helpMenu = new QMenu(MainWindow); helpMenu->setToolTipsVisible(true); if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { - actionReportBug = TranslatedAction(MainWindow); - actionReportBug->setObjectName(QStringLiteral("actionReportBug")); - actionReportBug->setIcon(APPLICATION->getThemedIcon("bug")); - actionReportBug.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Report a Bug")); - actionReportBug.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the bug tracker to report a bug with %1.")); - all_actions.append(&actionReportBug); helpMenu->addAction(actionReportBug); } if(!BuildConfig.MATRIX_URL.isEmpty()) { - actionMATRIX = TranslatedAction(MainWindow); - actionMATRIX->setObjectName(QStringLiteral("actionMATRIX")); - actionMATRIX->setIcon(APPLICATION->getThemedIcon("matrix")); - actionMATRIX.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Matrix space")); - actionMATRIX.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Matrix space")); - all_actions.append(&actionMATRIX); helpMenu->addAction(actionMATRIX); } if (!BuildConfig.DISCORD_URL.isEmpty()) { - actionDISCORD = TranslatedAction(MainWindow); - actionDISCORD->setObjectName(QStringLiteral("actionDISCORD")); - actionDISCORD->setIcon(APPLICATION->getThemedIcon("discord")); - actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Discord guild")); - actionDISCORD.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 Discord guild.")); - all_actions.append(&actionDISCORD); helpMenu->addAction(actionDISCORD); } if (!BuildConfig.SUBREDDIT_URL.isEmpty()) { - actionREDDIT = TranslatedAction(MainWindow); - actionREDDIT->setObjectName(QStringLiteral("actionREDDIT")); - actionREDDIT->setIcon(APPLICATION->getThemedIcon("reddit-alien")); - actionREDDIT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Subreddit")); - actionREDDIT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 subreddit.")); - all_actions.append(&actionREDDIT); helpMenu->addAction(actionREDDIT); } - actionAbout = TranslatedAction(MainWindow); - actionAbout->setObjectName(QStringLiteral("actionAbout")); - actionAbout->setIcon(APPLICATION->getThemedIcon("about")); - actionAbout->setMenuRole(QAction::AboutRole); - actionAbout.setTextId(QT_TRANSLATE_NOOP("MainWindow", "About %1")); - actionAbout.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View information about %1.")); - all_actions.append(&actionAbout); helpMenu->addAction(actionAbout); helpMenuButton = TranslatedToolButton(MainWindow); @@ -435,36 +470,13 @@ public: if(BuildConfig.UPDATER_ENABLED) { - actionCheckUpdate = TranslatedAction(MainWindow); - actionCheckUpdate->setObjectName(QStringLiteral("actionCheckUpdate")); - actionCheckUpdate->setIcon(APPLICATION->getThemedIcon("checkupdate")); - actionCheckUpdate.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Update")); - actionCheckUpdate.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Check for new updates for %1.")); - all_actions.append(&actionCheckUpdate); mainToolBar->addAction(actionCheckUpdate); } mainToolBar->addSeparator(); - actionCAT = TranslatedAction(MainWindow); - actionCAT->setObjectName(QStringLiteral("actionCAT")); - actionCAT->setCheckable(true); - actionCAT->setIcon(APPLICATION->getThemedIcon("cat")); - actionCAT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Meow")); - actionCAT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "It's a fluffy kitty :3")); - actionCAT->setPriority(QAction::LowPriority); - all_actions.append(&actionCAT); mainToolBar->addAction(actionCAT); - // profile menu and its actions - actionManageAccounts = TranslatedAction(MainWindow); - actionManageAccounts->setObjectName(QStringLiteral("actionManageAccounts")); - actionManageAccounts.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Manage Accounts")); - // FIXME: no tooltip! - actionManageAccounts->setCheckable(false); - actionManageAccounts->setIcon(APPLICATION->getThemedIcon("accounts")); - all_actions.append(&actionManageAccounts); - all_toolbars.append(&mainToolBar); MainWindow->addToolBar(Qt::TopToolBarArea, mainToolBar); } @@ -475,233 +487,132 @@ public: menuBar->setVisible(APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); createMenuActions(MainWindow); - // TODO: only enable options while an instance is selected (if applicable) fileMenu = menuBar->addMenu(tr("&File")); - fileMenu->addAction(newAct); - fileMenu->addAction(openAct); - fileMenu->addAction(openOfflineAct); - fileMenu->addAction(closeAct); + fileMenu->addAction(actionAddInstance); + fileMenu->addAction(actionLaunchInstance); + fileMenu->addAction(actionLaunchInstanceOffline); + fileMenu->addAction(actionCloseWindow); fileMenu->addSeparator(); - fileMenu->addAction(editInstanceAct); - fileMenu->addAction(editNotesAct); - fileMenu->addAction(editModsAct); - fileMenu->addAction(editWorldsAct); - fileMenu->addAction(manageScreenshotsAct); - fileMenu->addAction(changeGroupAct); + fileMenu->addAction(actionEditInstance); + fileMenu->addAction(actionEditInstNotes); + fileMenu->addAction(actionMods); + fileMenu->addAction(actionWorlds); + fileMenu->addAction(actionScreenshots); + fileMenu->addAction(actionChangeInstGroup); fileMenu->addSeparator(); - fileMenu->addAction(openMCFolderAct); - fileMenu->addAction(openConfigFolderAct); - fileMenu->addAction(openInstanceFolderAct); + fileMenu->addAction(actionViewSelectedMCFolder); + fileMenu->addAction(actionConfig_Folder); + fileMenu->addAction(actionViewSelectedInstFolder); fileMenu->addSeparator(); - fileMenu->addAction(exportInstanceAct); - fileMenu->addAction(deleteInstanceAct); - fileMenu->addAction(duplicateInstanceAct); + fileMenu->addAction(actionExportInstance); + fileMenu->addAction(actionDeleteInstance); + fileMenu->addAction(actionCopyInstance); fileMenu->addSeparator(); // TODO: functionality for edit actions. They're intended to be used where you can type text, e.g. notes. editMenu = menuBar->addMenu(tr("&Edit")); - editMenu->addAction(undoAct); - editMenu->addAction(redoAct); + editMenu->addAction(actionUndo); + editMenu->addAction(actionRedo); editMenu->addSeparator(); - editMenu->addAction(cutAct); - editMenu->addAction(copyAct); - editMenu->addAction(pasteAct); - editMenu->addAction(selectAllAct); + editMenu->addAction(actionCut); + editMenu->addAction(actionCopy); + editMenu->addAction(actionPaste); + editMenu->addAction(actionSelectAll); editMenu->addSeparator(); - editMenu->addAction(settingsAct); + editMenu->addAction(actionSettings); + + viewMenu = menuBar->addMenu(tr("&View")); + viewMenu->addAction(actionCAT); + viewMenu->addSeparator(); + + menuBar->addMenu(foldersMenu); profileMenu = menuBar->addMenu(tr("&Profiles")); - profileMenu->addAction(manageAccountAct); + profileMenu->addAction(actionManageAccounts); helpMenu = menuBar->addMenu(tr("&Help")); - helpMenu->addAction(aboutAct); - helpMenu->addAction(wikiAct); - helpMenu->addAction(newsAct); + helpMenu->addAction(actionAbout); + helpMenu->addAction(actionWiki); + helpMenu->addAction(actionNewsMenuBar); helpMenu->addSeparator(); - helpMenu->addAction(reportBugAct); - helpMenu->addAction(matrixAct); - helpMenu->addAction(discordAct); - helpMenu->addAction(redditAct); + if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) + helpMenu->addAction(actionReportBug); + if (!BuildConfig.MATRIX_URL.isEmpty()) + helpMenu->addAction(actionMATRIX); + if (!BuildConfig.DISCORD_URL.isEmpty()) + helpMenu->addAction(actionDISCORD); + if (!BuildConfig.SUBREDDIT_URL.isEmpty()) + helpMenu->addAction(actionREDDIT); + helpMenu->addSeparator(); + if(BuildConfig.UPDATER_ENABLED) + helpMenu->addAction(actionCheckUpdate); MainWindow->setMenuBar(menuBar); } - // If a keyboard shortcut is changed here, it must be changed below in keyPressEvent as well void createMenuActions(MainWindow *MainWindow) { - newAct = new QAction(tr("&New Instance..."), MainWindow); - newAct->setShortcuts(QKeySequence::New); - newAct->setStatusTip(tr("Create a new instance")); - connect(newAct, &QAction::triggered, MainWindow, &MainWindow::on_actionAddInstance_triggered); + actionCloseWindow = new QAction(tr("Close &Window"), MainWindow); + actionCloseWindow->setShortcut(QKeySequence::Close); + actionCloseWindow->setStatusTip(tr("Close the current window")); + connect(actionCloseWindow, &QAction::triggered, APPLICATION, &Application::closeCurrentWindow); - openAct = new QAction(tr("&Launch"), MainWindow); - openAct->setShortcuts(QKeySequence::Open); - openAct->setStatusTip(tr("Launch the selected instance")); - openAct->setEnabled(false); - connect(openAct, &QAction::triggered, MainWindow, &MainWindow::on_actionLaunchInstance_triggered); + actionUndo = new QAction(tr("&Undo"), MainWindow); + actionUndo->setShortcuts(QKeySequence::Undo); + actionUndo->setStatusTip(tr("Undo")); + actionUndo->setEnabled(false); - openOfflineAct = new QAction(tr("Launch &Offline"), MainWindow); - openOfflineAct->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); - openOfflineAct->setStatusTip(tr("Launch the selected instance in offline mode")); - openOfflineAct->setEnabled(false); - connect(openOfflineAct, &QAction::triggered, MainWindow, &MainWindow::on_actionLaunchInstanceOffline_triggered); + actionRedo = new QAction(tr("&Redo"), MainWindow); + actionRedo->setShortcuts(QKeySequence::Redo); + actionRedo->setStatusTip(tr("Redo")); + actionRedo->setEnabled(false); - editInstanceAct = new QAction(tr("Edit Inst&ance..."), MainWindow); - editInstanceAct->setShortcut(QKeySequence(tr("Ctrl+I"))); - editInstanceAct->setStatusTip(tr("Edit the selected instance")); - editInstanceAct->setEnabled(false); - connect(editInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionEditInstance_triggered); + actionCut = new QAction(tr("Cu&t"), MainWindow); + actionCut->setShortcuts(QKeySequence::Cut); + actionCut->setStatusTip(tr("Cut")); + actionCut->setEnabled(false); - editNotesAct = new QAction(tr("E&dit Notes..."), MainWindow); - editNotesAct->setStatusTip(tr("Edit the selected instance's notes")); - editNotesAct->setEnabled(false); - connect(editNotesAct, &QAction::triggered, MainWindow, &MainWindow::on_actionEditInstNotes_triggered); + actionCopy = new QAction(tr("&Copy"), MainWindow); + actionCopy->setShortcuts(QKeySequence::Copy); + actionCopy->setStatusTip(tr("Copy")); + actionCopy->setEnabled(false); - editModsAct = new QAction(tr("View &Mods"), MainWindow); - editModsAct->setStatusTip(tr("View the selected instance's mods")); - editModsAct->setEnabled(false); - connect(editModsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionMods_triggered); + actionPaste = new QAction(tr("&Paste"), MainWindow); + actionPaste->setShortcuts(QKeySequence::Paste); + actionPaste->setStatusTip(tr("Paste")); + actionPaste->setEnabled(false); - editWorldsAct = new QAction(tr("&View Worlds"), MainWindow); - editWorldsAct->setStatusTip(tr("View the selected instance's worlds")); - editWorldsAct->setEnabled(false); - connect(editWorldsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionWorlds_triggered); + actionSelectAll = new QAction(tr("Select &All"), MainWindow); + actionSelectAll->setShortcuts(QKeySequence::SelectAll); + actionSelectAll->setStatusTip(tr("Select all")); + actionSelectAll->setEnabled(false); - manageScreenshotsAct = new QAction(tr("Manage &Screenshots"), MainWindow); - manageScreenshotsAct->setStatusTip(tr("Manage the selected instance's screenshots")); - manageScreenshotsAct->setEnabled(false); - connect(manageScreenshotsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionScreenshots_triggered); + actionWiki = new QAction(tr("%1 He&lp").arg(BuildConfig.LAUNCHER_NAME), MainWindow); + actionWiki->setStatusTip(tr("Open the %1 wiki").arg(BuildConfig.LAUNCHER_NAME)); + connect(actionWiki, &QAction::triggered, MainWindow, &MainWindow::on_actionOpenWiki_triggered); - changeGroupAct = new QAction(tr("&Change Group..."), MainWindow); - changeGroupAct->setShortcut(QKeySequence(tr("Ctrl+G"))); - changeGroupAct->setStatusTip(tr("Change the selected instance's group")); - changeGroupAct->setEnabled(false); - connect(changeGroupAct, &QAction::triggered, MainWindow, &MainWindow::on_actionChangeInstGroup_triggered); - - openMCFolderAct = new QAction(tr("Open Minec&raft Folder"), MainWindow); - openMCFolderAct->setShortcut(QKeySequence(tr("Ctrl+M"))); - openMCFolderAct->setStatusTip(tr("Open the selected instance's Minecraft folder")); - openMCFolderAct->setEnabled(false); - connect(openMCFolderAct, &QAction::triggered, MainWindow, &MainWindow::on_actionViewSelectedMCFolder_triggered); - - openConfigFolderAct = new QAction(tr("&Open Confi&g Folder"), MainWindow); - openConfigFolderAct->setStatusTip(tr("Open the selected instance's config folder")); - openConfigFolderAct->setEnabled(false); - connect(openConfigFolderAct, &QAction::triggered, MainWindow, &MainWindow::on_actionConfig_Folder_triggered); - - openInstanceFolderAct = new QAction(tr("&Open &Instance Folder"), MainWindow); - openInstanceFolderAct->setStatusTip(tr("Open the selected instance's main folder")); - openInstanceFolderAct->setEnabled(false); - connect(openInstanceFolderAct, &QAction::triggered, MainWindow, &MainWindow::on_actionViewInstanceFolder_triggered); - - exportInstanceAct = new QAction(tr("E&xport Instance..."), MainWindow); - exportInstanceAct->setShortcut(QKeySequence(tr("Ctrl+E"))); - exportInstanceAct->setStatusTip(tr("Export the selected instance")); - exportInstanceAct->setEnabled(false); - connect(exportInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionExportInstance_triggered); - - deleteInstanceAct = new QAction(tr("Dele&te Instance..."), MainWindow); - deleteInstanceAct->setShortcuts({QKeySequence(tr("Backspace")), QKeySequence::Delete}); - deleteInstanceAct->setStatusTip(tr("Delete the selected instance")); - deleteInstanceAct->setEnabled(false); - connect(deleteInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionDeleteInstance_triggered); - - duplicateInstanceAct = new QAction(tr("Cop&y Instance..."), MainWindow); - duplicateInstanceAct->setShortcut(QKeySequence(tr("Ctrl+D"))); - duplicateInstanceAct->setStatusTip(tr("Duplicate the selected instance")); - duplicateInstanceAct->setEnabled(false); - connect(duplicateInstanceAct, &QAction::triggered, MainWindow, &MainWindow::on_actionCopyInstance_triggered); - - closeAct = new QAction(tr("Close &Window"), MainWindow); - closeAct->setShortcut(QKeySequence::Close); - closeAct->setStatusTip(tr("Close the current window")); - connect(closeAct, &QAction::triggered, APPLICATION, &Application::closeCurrentWindow); - - undoAct = new QAction(tr("&Undo"), MainWindow); - undoAct->setShortcuts(QKeySequence::Undo); - undoAct->setStatusTip(tr("Undo")); - undoAct->setEnabled(false); - - redoAct = new QAction(tr("&Redo"), MainWindow); - redoAct->setShortcuts(QKeySequence::Redo); - redoAct->setStatusTip(tr("Redo")); - redoAct->setEnabled(false); - - cutAct = new QAction(tr("Cu&t"), MainWindow); - cutAct->setShortcuts(QKeySequence::Cut); - cutAct->setStatusTip(tr("Cut")); - cutAct->setEnabled(false); - - copyAct = new QAction(tr("&Copy"), MainWindow); - copyAct->setShortcuts(QKeySequence::Copy); - copyAct->setStatusTip(tr("Copy")); - copyAct->setEnabled(false); - - pasteAct = new QAction(tr("&Paste"), MainWindow); - pasteAct->setShortcuts(QKeySequence::Paste); - pasteAct->setStatusTip(tr("Paste")); - pasteAct->setEnabled(false); - - selectAllAct = new QAction(tr("Select &All"), MainWindow); - selectAllAct->setShortcuts(QKeySequence::SelectAll); - selectAllAct->setStatusTip(tr("Select all")); - selectAllAct->setEnabled(false); - - settingsAct = new QAction(tr("&Settings..."), MainWindow); - settingsAct->setShortcut(QKeySequence::Preferences); - settingsAct->setStatusTip(tr("Change %1 settings").arg(BuildConfig.LAUNCHER_NAME)); - connect(settingsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionSettings_triggered); - - manageAccountAct = new QAction(tr("&Manage Accounts..."), MainWindow); - manageAccountAct->setStatusTip(tr("Open account manager")); - connect(manageAccountAct, &QAction::triggered, MainWindow, &MainWindow::on_actionManageAccounts_triggered); - - aboutAct = new QAction(tr("&About"), MainWindow); - aboutAct->setStatusTip(tr("About %1").arg(BuildConfig.LAUNCHER_NAME)); - connect(aboutAct, &QAction::triggered, MainWindow, &MainWindow::on_actionAbout_triggered); - - wikiAct = new QAction(tr("%1 He&lp").arg(BuildConfig.LAUNCHER_NAME), MainWindow); - wikiAct->setStatusTip(tr("Open %1's wiki").arg(BuildConfig.LAUNCHER_NAME)); - connect(wikiAct, &QAction::triggered, MainWindow, &MainWindow::on_actionOpenWiki_triggered); - - newsAct = new QAction(tr("&%1 &News").arg(BuildConfig.LAUNCHER_NAME), MainWindow); - newsAct->setStatusTip(tr("Open %1's news").arg(BuildConfig.LAUNCHER_NAME)); - connect(newsAct, &QAction::triggered, MainWindow, &MainWindow::on_actionMoreNews_triggered); - - reportBugAct = new QAction(tr("Report &Bugs..."), MainWindow); - reportBugAct->setStatusTip(tr("Report bugs to the developers")); - connect(reportBugAct, &QAction::triggered, MainWindow, &MainWindow::on_actionReportBug_triggered); - - matrixAct = new QAction(tr("&Matrix"), MainWindow); - matrixAct->setStatusTip(tr("Open %1's Matrix space").arg(BuildConfig.LAUNCHER_NAME)); - connect(matrixAct, &QAction::triggered, MainWindow, &MainWindow::on_actionMATRIX_triggered); - - discordAct = new QAction(tr("&Discord"), MainWindow); - discordAct->setStatusTip(tr("Open %1's Discord guild").arg(BuildConfig.LAUNCHER_NAME)); - connect(discordAct, &QAction::triggered, MainWindow, &MainWindow::on_actionDISCORD_triggered); - - redditAct = new QAction(tr("&Reddit"), MainWindow); - redditAct->setStatusTip(tr("Open %1's subreddit").arg(BuildConfig.LAUNCHER_NAME)); - connect(redditAct, &QAction::triggered, MainWindow, &MainWindow::on_actionREDDIT_triggered); + actionNewsMenuBar = new QAction(tr("&%1 &News").arg(BuildConfig.LAUNCHER_NAME), MainWindow); + actionNewsMenuBar->setStatusTip(tr("Open the development blog to read more news about %1.").arg(BuildConfig.LAUNCHER_NAME)); + connect(actionNewsMenuBar, &QAction::triggered, MainWindow, &MainWindow::on_actionMoreNews_triggered); } // "Instance actions" are actions that require an instance to be selected (i.e. "new instance" is not here) - void setInstanceActionsEnabled(bool enabled) const + void setInstanceActionsEnabled(bool enabled) { - openAct->setEnabled(enabled); - openOfflineAct->setEnabled(enabled); - editInstanceAct->setEnabled(enabled); - editNotesAct->setEnabled(enabled); - editModsAct->setEnabled(enabled); - editWorldsAct->setEnabled(enabled); - manageScreenshotsAct->setEnabled(enabled); - changeGroupAct->setEnabled(enabled); - openMCFolderAct->setEnabled(enabled); - openConfigFolderAct->setEnabled(enabled); - openInstanceFolderAct->setEnabled(enabled); - exportInstanceAct->setEnabled(enabled); - deleteInstanceAct->setEnabled(enabled); - duplicateInstanceAct->setEnabled(enabled); + actionLaunchInstance->setEnabled(enabled); + actionLaunchInstanceOffline->setEnabled(enabled); + actionEditInstance->setEnabled(enabled); + actionEditInstNotes->setEnabled(enabled); + actionMods->setEnabled(enabled); + actionWorlds->setEnabled(enabled); + actionScreenshots->setEnabled(enabled); + actionChangeInstGroup->setEnabled(enabled); + actionViewSelectedMCFolder->setEnabled(enabled); + actionConfig_Folder->setEnabled(enabled); + actionViewSelectedInstFolder->setEnabled(enabled); + actionExportInstance->setEnabled(enabled); + actionDeleteInstance->setEnabled(enabled); + actionCopyInstance->setEnabled(enabled); } void createStatusBar(QMainWindow *MainWindow) @@ -734,18 +645,8 @@ public: MainWindow->addToolBar(Qt::BottomToolBarArea, newsToolBar); } - void createInstanceToolbar(QMainWindow *MainWindow) + void createInstanceActions(QMainWindow *MainWindow) { - instanceToolBar = TranslatedToolbar(MainWindow); - instanceToolBar->setObjectName(QStringLiteral("instanceToolBar")); - // disabled until we have an instance selected - instanceToolBar->setEnabled(false); - instanceToolBar->setMovable(true); - instanceToolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea); - instanceToolBar->setToolButtonStyle(Qt::ToolButtonTextOnly); - instanceToolBar->setFloatable(false); - instanceToolBar->setWindowTitle(QT_TRANSLATE_NOOP("MainWindow", "Instance Toolbar")); - // NOTE: not added to toolbar, but used for instance context menu (right click) actionChangeInstIcon = TranslatedAction(MainWindow); actionChangeInstIcon->setObjectName(QStringLiteral("actionChangeInstIcon")); @@ -760,7 +661,6 @@ public: changeIconButton->setIcon(APPLICATION->getThemedIcon("news")); changeIconButton->setToolTip(actionChangeInstIcon->toolTip()); changeIconButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - instanceToolBar->addWidget(changeIconButton); // NOTE: not added to toolbar, but used for instance context menu (right click) actionRenameInstance = TranslatedAction(MainWindow); @@ -774,74 +674,61 @@ public: renameButton->setObjectName(QStringLiteral("renameButton")); renameButton->setToolTip(actionRenameInstance->toolTip()); renameButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - instanceToolBar->addWidget(renameButton); - - instanceToolBar->addSeparator(); actionLaunchInstance = TranslatedAction(MainWindow); actionLaunchInstance->setObjectName(QStringLiteral("actionLaunchInstance")); all_actions.append(&actionLaunchInstance); - instanceToolBar->addAction(actionLaunchInstance); actionLaunchInstanceOffline = TranslatedAction(MainWindow); actionLaunchInstanceOffline->setObjectName(QStringLiteral("actionLaunchInstanceOffline")); - actionLaunchInstanceOffline.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch Offline")); + actionLaunchInstanceOffline.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch &Offline")); actionLaunchInstanceOffline.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance in offline mode.")); all_actions.append(&actionLaunchInstanceOffline); - instanceToolBar->addAction(actionLaunchInstanceOffline); - - instanceToolBar->addSeparator(); actionEditInstance = TranslatedAction(MainWindow); actionEditInstance->setObjectName(QStringLiteral("actionEditInstance")); - actionEditInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Edit Instance")); + actionEditInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Edit Inst&ance...")); actionEditInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the instance settings, mods and versions.")); + actionEditInstance->setShortcut(QKeySequence(tr("Ctrl+I"))); all_actions.append(&actionEditInstance); - instanceToolBar->addAction(actionEditInstance); actionEditInstNotes = TranslatedAction(MainWindow); actionEditInstNotes->setObjectName(QStringLiteral("actionEditInstNotes")); - actionEditInstNotes.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Edit Notes")); + actionEditInstNotes.setTextId(QT_TRANSLATE_NOOP("MainWindow", "E&dit Notes...")); actionEditInstNotes.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Edit the notes for the selected instance.")); all_actions.append(&actionEditInstNotes); - instanceToolBar->addAction(actionEditInstNotes); actionMods = TranslatedAction(MainWindow); actionMods->setObjectName(QStringLiteral("actionMods")); - actionMods.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Mods")); + actionMods.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View &Mods")); actionMods.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View the mods of this instance.")); all_actions.append(&actionMods); - instanceToolBar->addAction(actionMods); actionWorlds = TranslatedAction(MainWindow); actionWorlds->setObjectName(QStringLiteral("actionWorlds")); - actionWorlds.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Worlds")); + actionWorlds.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&View Worlds")); actionWorlds.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View the worlds of this instance.")); all_actions.append(&actionWorlds); - instanceToolBar->addAction(actionWorlds); actionScreenshots = TranslatedAction(MainWindow); actionScreenshots->setObjectName(QStringLiteral("actionScreenshots")); - actionScreenshots.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Manage Screenshots")); + actionScreenshots.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Manage &Screenshots")); actionScreenshots.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View and upload screenshots for this instance.")); all_actions.append(&actionScreenshots); - instanceToolBar->addAction(actionScreenshots); actionChangeInstGroup = TranslatedAction(MainWindow); actionChangeInstGroup->setObjectName(QStringLiteral("actionChangeInstGroup")); - actionChangeInstGroup.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Change Group")); + actionChangeInstGroup.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Change Group...")); actionChangeInstGroup.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the selected instance's group.")); + actionChangeInstGroup->setShortcut(QKeySequence(tr("Ctrl+G"))); all_actions.append(&actionChangeInstGroup); - instanceToolBar->addAction(actionChangeInstGroup); - - instanceToolBar->addSeparator(); actionViewSelectedMCFolder = TranslatedAction(MainWindow); actionViewSelectedMCFolder->setObjectName(QStringLiteral("actionViewSelectedMCFolder")); - actionViewSelectedMCFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Minecraft Folder")); + actionViewSelectedMCFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Minec&raft Folder")); actionViewSelectedMCFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's Minecraft folder in a file browser.")); + actionViewSelectedMCFolder->setShortcut(QKeySequence(tr("Ctrl+M"))); all_actions.append(&actionViewSelectedMCFolder); - instanceToolBar->addAction(actionViewSelectedMCFolder); /* actionViewSelectedModsFolder = TranslatedAction(MainWindow); @@ -849,45 +736,89 @@ public: actionViewSelectedModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Mods Folder")); actionViewSelectedModsFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's mods folder in a file browser.")); all_actions.append(&actionViewSelectedModsFolder); - instanceToolBar->addAction(actionViewSelectedModsFolder); */ actionConfig_Folder = TranslatedAction(MainWindow); actionConfig_Folder->setObjectName(QStringLiteral("actionConfig_Folder")); - actionConfig_Folder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Config Folder")); + actionConfig_Folder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Confi&g Folder")); actionConfig_Folder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the instance's config folder.")); + // Qt on macOS is "smart" and will eat up this action when added to the menu bar because it starts with the word "config"... + // Docs: https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar + actionConfig_Folder->setMenuRole(QAction::NoRole); all_actions.append(&actionConfig_Folder); - instanceToolBar->addAction(actionConfig_Folder); actionViewSelectedInstFolder = TranslatedAction(MainWindow); actionViewSelectedInstFolder->setObjectName(QStringLiteral("actionViewSelectedInstFolder")); - actionViewSelectedInstFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Instance Folder")); + actionViewSelectedInstFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Instance Folder")); actionViewSelectedInstFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's root folder in a file browser.")); all_actions.append(&actionViewSelectedInstFolder); - instanceToolBar->addAction(actionViewSelectedInstFolder); - - instanceToolBar->addSeparator(); actionExportInstance = TranslatedAction(MainWindow); actionExportInstance->setObjectName(QStringLiteral("actionExportInstance")); - actionExportInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Export Instance")); + actionExportInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "E&xport Instance...")); actionExportInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Export the selected instance as a zip file.")); + actionExportInstance->setShortcut(QKeySequence(tr("Ctrl+E"))); all_actions.append(&actionExportInstance); - instanceToolBar->addAction(actionExportInstance); actionDeleteInstance = TranslatedAction(MainWindow); actionDeleteInstance->setObjectName(QStringLiteral("actionDeleteInstance")); - actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Delete Instance")); + actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Dele&te Instance...")); actionDeleteInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Delete the selected instance.")); + actionDeleteInstance->setShortcuts({QKeySequence(tr("Backspace")), QKeySequence::Delete}); all_actions.append(&actionDeleteInstance); - instanceToolBar->addAction(actionDeleteInstance); actionCopyInstance = TranslatedAction(MainWindow); actionCopyInstance->setObjectName(QStringLiteral("actionCopyInstance")); actionCopyInstance->setIcon(APPLICATION->getThemedIcon("copy")); - actionCopyInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Copy Instance")); + actionCopyInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Cop&y Instance...")); actionCopyInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Copy the selected instance.")); + actionCopyInstance->setShortcut(QKeySequence(tr("Ctrl+D"))); all_actions.append(&actionCopyInstance); + + } + + void createInstanceToolbar(QMainWindow *MainWindow) + { + instanceToolBar = TranslatedToolbar(MainWindow); + instanceToolBar->setObjectName(QStringLiteral("instanceToolBar")); + // disabled until we have an instance selected + instanceToolBar->setEnabled(false); + instanceToolBar->setMovable(true); + instanceToolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea); + instanceToolBar->setToolButtonStyle(Qt::ToolButtonTextOnly); + instanceToolBar->setFloatable(false); + instanceToolBar->setWindowTitle(QT_TRANSLATE_NOOP("MainWindow", "Instance Toolbar")); + + instanceToolBar->addWidget(changeIconButton); + instanceToolBar->addWidget(renameButton); + + instanceToolBar->addSeparator(); + + instanceToolBar->addAction(actionLaunchInstance); + instanceToolBar->addAction(actionLaunchInstanceOffline); + + instanceToolBar->addSeparator(); + + instanceToolBar->addAction(actionEditInstance); + instanceToolBar->addAction(actionEditInstNotes); + instanceToolBar->addAction(actionMods); + instanceToolBar->addAction(actionWorlds); + instanceToolBar->addAction(actionScreenshots); + instanceToolBar->addAction(actionChangeInstGroup); + + instanceToolBar->addSeparator(); + + instanceToolBar->addAction(actionViewSelectedMCFolder); + /* + instanceToolBar->addAction(actionViewSelectedModsFolder); + */ + instanceToolBar->addAction(actionConfig_Folder); + instanceToolBar->addAction(actionViewSelectedInstFolder); + + instanceToolBar->addSeparator(); + + instanceToolBar->addAction(actionExportInstance); + instanceToolBar->addAction(actionDeleteInstance); instanceToolBar->addAction(actionCopyInstance); all_toolbars.append(&instanceToolBar); @@ -907,6 +838,9 @@ public: MainWindow->setAccessibleName(BuildConfig.LAUNCHER_NAME); #endif + createMainToolbarActions(MainWindow); + createInstanceActions(MainWindow); + createMenuBar(dynamic_cast(MainWindow)); createMainToolbar(MainWindow); @@ -1138,57 +1072,6 @@ void MainWindow::keyReleaseEvent(QKeyEvent *event) else QMainWindow::keyReleaseEvent(event); } - -// FIXME: This is a hack because keyboard shortcuts do nothing while menu bar is hidden on systems without native menu bar -// If a keyboard shortcut is changed above in `createMenuActions`, it must be changed here as well -void MainWindow::keyPressEvent(QKeyEvent *event) -{ - if(ui->menuBar->isVisible() || ui->menuBar->isNativeMenuBar()) - { - QMainWindow::keyPressEvent(event); - return; // let the menu bar handle the keyboard shortcuts - } - - if(event->modifiers().testFlag(Qt::ControlModifier)) - { - switch(event->key()) - { - case Qt::Key_N: - on_actionAddInstance_triggered(); - return; - case Qt::Key_O: - if(event->modifiers().testFlag(Qt::ShiftModifier)) - on_actionLaunchInstanceOffline_triggered(); - else - on_actionLaunchInstance_triggered(); - return; - case Qt::Key_I: - on_actionEditInstance_triggered(); - return; - case Qt::Key_G: - on_actionChangeInstGroup_triggered(); - return; - case Qt::Key_M: - on_actionViewSelectedMCFolder_triggered(); - return; - case Qt::Key_E: - on_actionExportInstance_triggered(); - return; - case Qt::Key_Delete: - on_actionDeleteInstance_triggered(); - return; - case Qt::Key_D: - on_actionCopyInstance_triggered(); - return; - case Qt::Key_W: - close(); - return; - // Text editing shortcuts are handled by the OS, so they do not need to be implemented here again - default: - return; - } - } -} #endif void MainWindow::retranslateUi() @@ -1333,7 +1216,9 @@ void MainWindow::updateToolsMenu() } QAction *normalLaunch = launchMenu->addAction(tr("Launch")); + normalLaunch->setShortcut(QKeySequence::Open); QAction *normalLaunchOffline = launchOfflineMenu->addAction(tr("Launch Offline")); + normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); connect(normalLaunch, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, true); @@ -1454,7 +1339,7 @@ void MainWindow::repopulateAccountsMenu() accountMenu->addSeparator(); ui->profileMenu->addSeparator(); accountMenu->addAction(ui->actionManageAccounts); - ui->profileMenu->addAction(ui->manageAccountAct); + ui->profileMenu->addAction(ui->actionManageAccounts); } void MainWindow::updatesAllowedChanged(bool allowed) diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 5424a4a9..2032acba 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -190,8 +190,6 @@ private slots: #ifndef Q_OS_MAC void keyReleaseEvent(QKeyEvent *event) override; - - void keyPressEvent(QKeyEvent *event) override; #endif private: From be82f4db9eafa63c7497c2abe3449b83b6babd00 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 16 Apr 2022 10:10:13 -0300 Subject: [PATCH 260/605] libs: Don't force bundled libs Now that QuaZip 1.3 is released, packages from package managers can include the patch needed for PolyMC, so we can use the users system libraries if available. --- CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1350a3ba..2c36d847 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,8 +107,7 @@ set(Launcher_DISCORD_URL "https://discord.gg/Z52pwxWCHP" CACHE STRING "URL for t set(Launcher_SUBREDDIT_URL "https://www.reddit.com/r/PolyMCLauncher/" CACHE STRING "URL for the subreddit.") # Builds -# TODO: Launcher_FORCE_BUNDLED_LIBS should be off in the future, but as of QuaZip 1.2, we can't do that yet. -set(Launcher_FORCE_BUNDLED_LIBS ON CACHE BOOL "Prevent using system libraries, if they are available as submodules") +set(Launcher_FORCE_BUNDLED_LIBS OFF CACHE BOOL "Prevent using system libraries, if they are available as submodules") set(Launcher_QT_VERSION_MAJOR "5" CACHE STRING "Major Qt version to build against") From af167e8e6702efb9232a6a99f5e64cf7a92aabad Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 16 Apr 2022 10:17:33 -0300 Subject: [PATCH 261/605] libs: update bundled submodules --- libraries/libnbtplusplus | 2 +- libraries/quazip | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/libnbtplusplus b/libraries/libnbtplusplus index 129be45a..2203af7e 160000 --- a/libraries/libnbtplusplus +++ b/libraries/libnbtplusplus @@ -1 +1 @@ -Subproject commit 129be45a7f91920e76673af104534d215c497d85 +Subproject commit 2203af7eeb48c45398139b583615134efd8d407f diff --git a/libraries/quazip b/libraries/quazip index 09ec1d10..6117161a 160000 --- a/libraries/quazip +++ b/libraries/quazip @@ -1 +1 @@ -Subproject commit 09ec1d10c6d627f895109b21728dda000cbfa7d1 +Subproject commit 6117161af08e366c37499895b00ef62f93adc345 From ba020fbd2176460021b0f552b7178af9562bd4bf Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 16 Apr 2022 11:23:42 -0300 Subject: [PATCH 262/605] fix: Don't error when not finding valid system quazip --- CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c36d847..9530ff10 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,7 +134,7 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 5) find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml) if(NOT Launcher_FORCE_BUNDLED_LIBS) - find_package(QuaZip-Qt5 REQUIRED) + find_package(QuaZip-Qt5 1.3) 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) @@ -276,6 +276,8 @@ if (FORCE_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/iconfix) # fork of Qt's QIcon loader From 90d4acd1a1c9abeb31548d152c155491e2cf98fa Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 23 Mar 2022 13:20:14 +0100 Subject: [PATCH 263/605] refactor: combine portable and system builds Portable builds now have the same layout as system builds. If you want to build a portable bundle, you now need to additionally install the `portable` component. For example: $ cmake -Bbuild -DCMAKE_INSTALL_PREFIX=install ... $ cmake --build build $ cmake --install build $ cmake --install build --component portable --- CMakeLists.txt | 56 +++++++++++++++---------------------- launcher/Application.cpp | 53 ++++++++++++++++++----------------- launcher/Launcher.in | 2 +- program_info/CMakeLists.txt | 2 ++ program_info/portable.txt | 4 +++ 5 files changed, 58 insertions(+), 59 deletions(-) create mode 100644 program_info/portable.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 1350a3ba..cf06fe0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -165,13 +165,12 @@ add_subdirectory(program_info) ####################################### Install layout ####################################### -# Install the build results according to platform -set(Launcher_PORTABLE 1 CACHE BOOL "The type of installation (Portable or System)") +# TODO: drop this? +# Target install directory, relative to CMAKE_INSTALL_PREFIx +set(BUNDLE_DEST_DIR ".") -if (Launcher_PORTABLE) - # launcher/Application.cpp will use this value - set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_PORTABLE") -endif() +# Install "portable.txt" if selected component is "portable" +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Portable_File}" DESTINATION ${BUNDLE_DEST_DIR} COMPONENT portable EXCLUDE_FROM_ALL) if(UNIX AND APPLE) set(BINARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") @@ -180,8 +179,6 @@ if(UNIX AND APPLE) set(RESOURCES_DEST_DIR "${Launcher_Name}.app/Contents/Resources") set(JARS_DEST_DIR "${Launcher_Name}.app/Contents/MacOS/jars") - set(BUNDLE_DEST_DIR ".") - # Apps to bundle set(APPS "\${CMAKE_INSTALL_PREFIX}/${Launcher_Name}.app") @@ -206,30 +203,12 @@ if(UNIX AND APPLE) elseif(UNIX) set(BINARY_DEST_DIR "bin") - if(Launcher_PORTABLE) - set(LIBRARY_DEST_DIR "bin") - set(BUNDLE_DEST_DIR ".") - set(JARS_DEST_DIR "bin/jars") - - # Install basic runner script - configure_file(launcher/Launcher.in "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" @ONLY) - install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" DESTINATION ${BUNDLE_DEST_DIR} RENAME ${Launcher_Name}) - else() - set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}") - set(JARS_DEST_DIR "share/jars") - set(LAUNCHER_DESKTOP_DEST_DIR "share/applications" CACHE STRING "Path to the desktop file directory") - set(LAUNCHER_METAINFO_DEST_DIR "share/metainfo" CACHE STRING "Path to the metainfo directory") - set(LAUNCHER_ICON_DEST_DIR "share/icons/hicolor/scalable/apps" CACHE STRING "Path to the scalable icon directory") - set(LAUNCHER_MAN_DEST_DIR "share/man/man6" CACHE STRING "Path to the man page directory") - - # jars path is determined on runtime, relative to "Application root path", generally /usr for Launcher_PORTABLE=0 - set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_JARS_LOCATION=${JARS_DEST_DIR}") - - install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${LAUNCHER_DESKTOP_DEST_DIR}) - install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${LAUNCHER_METAINFO_DEST_DIR}) - install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION ${LAUNCHER_ICON_DEST_DIR}) - install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_ManPage} DESTINATION ${LAUNCHER_MAN_DEST_DIR} RENAME "${Launcher_APP_BINARY_NAME}.6") - endif() + set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}") + set(JARS_DEST_DIR "share/jars") + set(LAUNCHER_DESKTOP_DEST_DIR "share/applications" CACHE STRING "Path to the desktop file directory") + set(LAUNCHER_METAINFO_DEST_DIR "share/metainfo" CACHE STRING "Path to the metainfo directory") + set(LAUNCHER_ICON_DEST_DIR "share/icons/hicolor/scalable/apps" CACHE STRING "Path to the scalable icon directory") + set(LAUNCHER_MAN_DEST_DIR "share/man/man6" CACHE STRING "Path to the man page directory") # install as bundle with no dependencies included set(INSTALL_BUNDLE "nodeps") @@ -237,11 +216,22 @@ elseif(UNIX) # Set RPATH SET(Launcher_BINARY_RPATH "$ORIGIN/") + # jars path is determined on runtime, relative to "Application root path", generally /usr or the root of the portable bundle + set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_JARS_LOCATION=${JARS_DEST_DIR}") + + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${LAUNCHER_DESKTOP_DEST_DIR}) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${LAUNCHER_METAINFO_DEST_DIR}) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION ${LAUNCHER_ICON_DEST_DIR}) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_ManPage} DESTINATION ${LAUNCHER_MAN_DEST_DIR} RENAME "${Launcher_APP_BINARY_NAME}.6") + + # Install basic runner script if component "portable" is selected + configure_file(launcher/Launcher.in "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" @ONLY) + install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" DESTINATION ${BUNDLE_DEST_DIR} RENAME ${Launcher_Name} COMPONENT portable EXCLUDE_FROM_ALL) + elseif(WIN32) set(BINARY_DEST_DIR ".") set(LIBRARY_DEST_DIR ".") set(PLUGIN_DEST_DIR ".") - set(BUNDLE_DEST_DIR ".") set(RESOURCES_DEST_DIR ".") set(JARS_DEST_DIR "jars") diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 9cca534c..a9f7a0f0 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -316,6 +316,26 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) QString origcwdPath = QDir::currentPath(); QString binPath = applicationDirPath(); + + { + // Root path is used for updates and portable data +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr + m_rootPath = foo.absolutePath(); +#elif defined(Q_OS_WIN32) + m_rootPath = binPath; +#elif defined(Q_OS_MAC) + QDir foo(FS::PathCombine(binPath, "../..")); + m_rootPath = foo.absolutePath(); + // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues) + FS::updateTimestamp(m_rootPath); +#endif + +#ifdef LAUNCHER_JARS_LOCATION + m_jarsPath = TOSTRING(LAUNCHER_JARS_LOCATION); +#endif + } + QString adjustedBy; QString dataPath; // change folder @@ -324,15 +344,14 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) { // the dir param. it makes multimc data path point to whatever the user specified // on command line - adjustedBy += "Command line " + dirParam; + adjustedBy = "Command line"; dataPath = dirParam; } else { -#if !defined(LAUNCHER_PORTABLE) || defined(Q_OS_MAC) QDir foo(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); dataPath = foo.absolutePath(); - adjustedBy += dataPath; + adjustedBy = "Persistent data path"; #ifdef Q_OS_LINUX // TODO: this should be removed in a future version @@ -340,13 +359,14 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) QDir bar(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation), "polymc")); if (bar.exists()) { dataPath = bar.absolutePath(); - adjustedBy += "Legacy data path " + dataPath; + adjustedBy = "Legacy data path"; } #endif -#else - dataPath = applicationDirPath(); - adjustedBy += "Fallback to binary path " + dataPath; -#endif + + if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { + dataPath = m_rootPath; + adjustedBy = "Portable data path"; + } } if (!FS::ensureFolderPathExists(dataPath)) @@ -535,24 +555,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) qDebug() << "<> Log initialized."; } - // Set up paths { - // Root path is used for updates. -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - QDir foo(FS::PathCombine(binPath, "..")); - m_rootPath = foo.absolutePath(); -#elif defined(Q_OS_WIN32) - m_rootPath = binPath; -#elif defined(Q_OS_MAC) - QDir foo(FS::PathCombine(binPath, "../..")); - m_rootPath = foo.absolutePath(); - // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues) - FS::updateTimestamp(m_rootPath); -#endif - -#ifdef LAUNCHER_JARS_LOCATION - m_jarsPath = TOSTRING(LAUNCHER_JARS_LOCATION); -#endif qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT; qDebug() << "Version : " << BuildConfig.printableVersionString(); diff --git a/launcher/Launcher.in b/launcher/Launcher.in index 5e5e2c2b..528e360e 100755 --- a/launcher/Launcher.in +++ b/launcher/Launcher.in @@ -21,7 +21,7 @@ echo "Launcher Dir: ${LAUNCHER_DIR}" # Set up env - filter out input LD_ variables but pass them in under different names export GAME_LIBRARY_PATH=${GAME_LIBRARY_PATH-${LD_LIBRARY_PATH}} export GAME_PRELOAD=${GAME_PRELOAD-${LD_PRELOAD}} -export LD_LIBRARY_PATH="${LAUNCHER_DIR}/bin":$LAUNCHER_LIBRARY_PATH +export LD_LIBRARY_PATH="${LAUNCHER_DIR}/lib@LIB_SUFFIX@":$LAUNCHER_LIBRARY_PATH export LD_PRELOAD=$LAUNCHER_PRELOAD export QT_PLUGIN_PATH="${LAUNCHER_DIR}/plugins" export QT_FONTPATH="${LAUNCHER_DIR}/fonts" diff --git a/program_info/CMakeLists.txt b/program_info/CMakeLists.txt index 9c243826..60549d8d 100644 --- a/program_info/CMakeLists.txt +++ b/program_info/CMakeLists.txt @@ -17,5 +17,7 @@ set(Launcher_Branding_ICNS "program_info/polymc.icns" PARENT_SCOPE) set(Launcher_Branding_WindowsRC "program_info/polymc.rc" PARENT_SCOPE) set(Launcher_Branding_LogoQRC "program_info/polymc.qrc" PARENT_SCOPE) +set(Launcher_Portable_File "program_info/portable.txt" PARENT_SCOPE) + configure_file(org.polymc.PolyMC.desktop.in org.polymc.PolyMC.desktop) configure_file(org.polymc.PolyMC.metainfo.xml.in org.polymc.PolyMC.metainfo.xml) diff --git a/program_info/portable.txt b/program_info/portable.txt new file mode 100644 index 00000000..b7e256a2 --- /dev/null +++ b/program_info/portable.txt @@ -0,0 +1,4 @@ +This file enables the portable mode for the launcher. + +If this file is present in the root directory of the launcher, it will store all data here. Otherwise it will store your data in your appdata directory. +You can safely delete this file, if you don't want the launcher to store your data here. From 6ed130fc1610da8d66ed17be9446e8f53bf96db5 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 23 Mar 2022 14:38:58 +0100 Subject: [PATCH 264/605] fix: don't allow portable builds on macOS --- CMakeLists.txt | 6 ++++-- launcher/Application.cpp | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cf06fe0d..5061de3b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -169,8 +169,10 @@ add_subdirectory(program_info) # Target install directory, relative to CMAKE_INSTALL_PREFIx set(BUNDLE_DEST_DIR ".") -# Install "portable.txt" if selected component is "portable" -install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Portable_File}" DESTINATION ${BUNDLE_DEST_DIR} COMPONENT portable EXCLUDE_FROM_ALL) +if(NOT (UNIX AND APPLE)) + # Install "portable.txt" if selected component is "portable" + install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Portable_File}" DESTINATION ${BUNDLE_DEST_DIR} COMPONENT portable EXCLUDE_FROM_ALL) +endif() if(UNIX AND APPLE) set(BINARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") diff --git a/launcher/Application.cpp b/launcher/Application.cpp index a9f7a0f0..6e934fa4 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -363,10 +363,12 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) } #endif +#ifndef Q_OS_MACOS if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { dataPath = m_rootPath; adjustedBy = "Portable data path"; } +#endif } if (!FS::ensureFolderPathExists(dataPath)) From b10d4d3b8f68a27434d04ac6c595cea0bc1337af Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 23 Mar 2022 14:42:56 +0100 Subject: [PATCH 265/605] fix: drop BUNDLE_DEST_DIR --- CMakeLists.txt | 8 ++------ launcher/CMakeLists.txt | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5061de3b..1f4f4295 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -165,13 +165,9 @@ add_subdirectory(program_info) ####################################### Install layout ####################################### -# TODO: drop this? -# Target install directory, relative to CMAKE_INSTALL_PREFIx -set(BUNDLE_DEST_DIR ".") - if(NOT (UNIX AND APPLE)) # Install "portable.txt" if selected component is "portable" - install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Portable_File}" DESTINATION ${BUNDLE_DEST_DIR} COMPONENT portable EXCLUDE_FROM_ALL) + install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Portable_File}" DESTINATION "." COMPONENT portable EXCLUDE_FROM_ALL) endif() if(UNIX AND APPLE) @@ -228,7 +224,7 @@ elseif(UNIX) # Install basic runner script if component "portable" is selected configure_file(launcher/Launcher.in "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" @ONLY) - install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" DESTINATION ${BUNDLE_DEST_DIR} RENAME ${Launcher_Name} COMPONENT portable EXCLUDE_FROM_ALL) + install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" DESTINATION "." RENAME ${Launcher_Name} COMPONENT portable EXCLUDE_FROM_ALL) elseif(WIN32) set(BINARY_DEST_DIR ".") diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 0ab398e8..6ed86726 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -991,7 +991,7 @@ if(DEFINED Launcher_APP_BINARY_DEFS) endif() install(TARGETS ${Launcher_Name} - BUNDLE DESTINATION ${BUNDLE_DEST_DIR} COMPONENT Runtime + BUNDLE DESTINATION "." COMPONENT Runtime LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime ) From 4a971226e4292bb7de9646c199582ab70ef002c5 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 23 Mar 2022 13:52:37 +0100 Subject: [PATCH 266/605] refactor(actions): combine steps for unified builds --- .github/workflows/build.yml | 243 +++++++++++++++++------------------- 1 file changed, 113 insertions(+), 130 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0123c99a..ab80af7e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,14 +17,6 @@ jobs: - os: ubuntu-20.04 - - os: ubuntu-20.04 - portable: true - - - os: ubuntu-20.04 - qt_version: 5.15.2 - qt_host: linux - app_image: true - - os: windows-2022 name: "Windows-i686" msystem: mingw32 @@ -33,16 +25,6 @@ jobs: name: "Windows-x86_64" msystem: mingw64 - - os: windows-2022 - name: "Windows-i686-portable" - msystem: mingw32 - portable: true - - - os: windows-2022 - name: "Windows-x86_64-portable" - msystem: mingw64 - portable: true - - os: macos-11 qt_version: 5.12.12 qt_host: mac @@ -53,9 +35,14 @@ jobs: env: MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} INSTALL_DIR: "install" + INSTALL_PORTABLE_DIR: "install-portable" + INSTALL_APPIMAGE_DIR: "install-appdir" BUILD_DIR: "build" steps: + ## + # PREPARE + ## - name: Checkout uses: actions/checkout@v3 with: @@ -87,16 +74,16 @@ jobs: distribution: 'temurin' java-version: '17' - - name: Cache Qt - if: runner.os != 'Windows' + - name: Cache Qt (macOS) + if: runner.os == 'macOS' id: cache-qt uses: actions/cache@v3 with: path: "${{ github.workspace }}/Qt/" key: ${{ runner.os }}-${{ matrix.qt_version }}-${{ matrix.qt_arch }}-qt_cache - - name: Install Qt - if: runner.os != 'Linux' && runner.os != 'Windows' || matrix.app_image == true + - name: Install Qt (macOS) + if: runner.os == 'macOS' uses: jurplel/install-qt-action@v2 with: version: ${{ matrix.qt_version }} @@ -105,8 +92,8 @@ jobs: cached: ${{ steps.cache-qt.outputs.cache-hit }} dir: "${{ github.workspace }}/Qt/" - - name: Install System Qt on Linux - if: runner.os == 'Linux' && matrix.app_image != true + - name: Install Qt (Linux) + if: runner.os == 'Linux' run: | sudo apt-get -y update sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 @@ -115,174 +102,170 @@ jobs: if: runner.os != 'Windows' uses: urkle/action-get-ninja@v1 - - name: Download linuxdeploy family for AppImage on Linux - if: matrix.app_image == true + - name: Prepare AppImage (Linux) + if: runner.os == 'Linux' run: | wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" wget "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage" wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" - - name: Download JREs for AppImage on Linux - if: matrix.app_image == true - shell: bash - run: | ${{ github.workspace }}/.github/scripts/prepare_JREs.sh - - name: Configure CMake - if: runner.os != 'Linux' && runner.os != 'Windows' + ## + # CONFIGURE + ## + + - name: Configure CMake (macOS) + if: runner.os == 'macOS' run: | cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -G Ninja - - name: Configure CMake on Windows - if: runner.os == 'Windows' && matrix.portable != true + - name: Configure CMake (Windows) + if: runner.os == 'Windows' shell: msys2 {0} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_PORTABLE=OFF -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -G Ninja - - name: Configure CMake on Windows portable - if: runner.os == 'Windows' && matrix.portable == true - shell: msys2 {0} + - name: Configure CMake (Linux) + if: runner.os == 'Linux' run: | cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -G Ninja - - name: Configure CMake on Linux - if: runner.os == 'Linux' && matrix.portable != true - run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DLauncher_PORTABLE=OFF -DENABLE_LTO=ON -G Ninja - - - name: Configure CMake on Linux Portable - if: runner.os == 'Linux' && matrix.portable == true - run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -G Ninja + ## + # BUILD + ## - name: Build if: runner.os != 'Windows' run: | cmake --build ${{ env.BUILD_DIR }} - - name: Build on Windows + - name: Build (Windows) if: runner.os == 'Windows' shell: msys2 {0} run: | cmake --build ${{ env.BUILD_DIR }} - - name: Install - if: runner.os != 'Linux' && runner.os != 'Windows' + ## + # PACKAGE BUILDS + ## + + - name: Package (macOS) + if: runner.os == 'macOS' run: | cmake --install ${{ env.BUILD_DIR }} - - name: Install on Windows + cd ${{ env.INSTALL_DIR }} + macdeployqt "PolyMC.app" -executable="PolyMC.app/Contents/MacOS/polymc" -always-overwrite + chmod +x "PolyMC.app/Contents/MacOS/polymc" + tar -czf ../PolyMC.tar.gz * + + - name: Package (Windows) if: runner.os == 'Windows' shell: msys2 {0} run: | cmake --install ${{ env.BUILD_DIR }} - - name: Install on Linux - if: runner.os == 'Linux' && matrix.portable != true - run: | - DESTDIR=${{ env.INSTALL_DIR }} cmake --install ${{ env.BUILD_DIR }} + cd ${{ env.INSTALL_DIR }} + if [ "${{ matrix.msystem }}" == "mingw32" ]; then + cp /mingw32/bin/libcrypto-1_1.dll /mingw32/bin/libssl-1_1.dll ./ + elif [ "${{ matrix.msystem }}" == "mingw64" ]; then + cp /mingw64/bin/libcrypto-1_1-x64.dll /mingw64/bin/libssl-1_1-x64.dll ./ + fi - - name: Install on Linux portable - if: runner.os == 'Linux' && matrix.portable == true + - name: Package (Windows, portable) + if: runner.os == 'Windows' + shell: msys2 {0} run: | - cmake --install ${{ env.BUILD_DIR }} + 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 - - name: Bundle AppImage - if: matrix.app_image == true + - name: Package (Linux) + if: runner.os == 'Linux' + run: | + cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_DIR }} + + cd ${{ env.INSTALL_DIR }} + tar -czf ../PolyMC.tar.gz * + + - name: Package (Linux, portable) + if: runner.os == 'Linux' + run: | + cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} + cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable + + cd ${{ env.INSTALL_PORTABLE_DIR }} + tar -czf ../PolyMC-portable.tar.gz * + + - name: Package AppImage (Linux) + if: runner.os == 'Linux' shell: bash run: | + cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr + export OUTPUT="PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage" chmod +x linuxdeploy-*.AppImage - mkdir -p ${{ env.INSTALL_DIR }}/usr/lib/jvm/java-{8,17}-openjdk + mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-{8,17}-openjdk - cp -r ${{ github.workspace }}/JREs/jre8/* ${{ env.INSTALL_DIR }}/usr/lib/jvm/java-8-openjdk + cp -r ${{ github.workspace }}/JREs/jre8/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk - cp -r ${{ github.workspace }}/JREs/jre17/* ${{ env.INSTALL_DIR }}/usr/lib/jvm/java-17-openjdk + cp -r ${{ github.workspace }}/JREs/jre17/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk - export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_DIR }}/usr/lib" - LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server" - LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64" - LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_DIR }}/usr/lib/jvm/java-17-openjdk/lib/server" - LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_DIR }}/usr/lib/jvm/java-17-openjdk/lib" + LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib" + LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server" + LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64" + LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk/lib/server" + LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk/lib" + export LD_LIBRARY_PATH - ./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_DIR }}/usr/share/icons/hicolor/scalable/apps/org.polymc.PolyMC.svg + ./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.polymc.PolyMC.svg - - name: Run macdeployqt + ## + # UPLOAD BUILDS + ## + + - name: Upload binary tarball (macOS) if: runner.os == 'macOS' - run: | - cd ${{ env.INSTALL_DIR }} - macdeployqt "PolyMC.app" -executable="PolyMC.app/Contents/MacOS/polymc" -always-overwrite - - - name: chmod binary on macOS - if: runner.os == 'macOS' - run: | - chmod +x "${{ github.workspace }}/${{ env.INSTALL_DIR }}/PolyMC.app/Contents/MacOS/polymc" - - - name: tar bundle on macOS - if: runner.os == 'macOS' - run: | - cd ${{ env.INSTALL_DIR }} - tar -czf ../PolyMC.tar.gz * - - - name: tar on Linux - if: runner.os == 'Linux' && matrix.app_image != true && matrix.portable != true - run: | - cd ${{ env.INSTALL_DIR }} - tar -czf ../PolyMC.tar.gz * - - - name: tar on Linux portable - if: runner.os == 'Linux' && matrix.app_image != true && matrix.portable == true - run: | - cd ${{ env.INSTALL_DIR }} - tar -czf ../PolyMC-portable.tar.gz * - - - name: Upload Linux tar.gz - if: runner.os == 'Linux' && matrix.app_image != true && matrix.portable != true uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC.tar.gz - - name: Upload Linux Portable tar.gz - if: runner.os == 'Linux' && matrix.app_image != true && matrix.portable == true - uses: actions/upload-artifact@v3 - with: - name: PolyMC-${{ runner.os }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} - path: PolyMC-portable.tar.gz - - - name: Upload AppImage for Linux - if: matrix.app_image == true - uses: actions/upload-artifact@v3 - with: - name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage - path: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage - - - name: Copy OpenSSL libs on Windows x86 - if: runner.os == 'Windows' && matrix.msystem == 'mingw32' - shell: msys2 {0} - run: | - cp /mingw32/bin/libcrypto-1_1.dll ${{ env.INSTALL_DIR }}/ - cp /mingw32/bin/libssl-1_1.dll ${{ env.INSTALL_DIR }}/ - - - name: Copy OpenSSL libs on Windows x86_64 - if: runner.os == 'Windows' && matrix.msystem == 'mingw64' - shell: msys2 {0} - run: | - cp /mingw64/bin/libcrypto-1_1-x64.dll ${{ env.INSTALL_DIR }}/ - cp /mingw64/bin/libssl-1_1-x64.dll ${{ env.INSTALL_DIR }}/ - - - name: Upload package for Windows + - name: Upload binary zip (Windows) if: runner.os == 'Windows' uses: actions/upload-artifact@v3 with: name: PolyMC-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} path: ${{ env.INSTALL_DIR }}/** - - name: Upload package for macOS - if: runner.os == 'macOS' + - name: Upload binary zip (Windows, portable) + if: runner.os == 'Windows' + uses: actions/upload-artifact@v3 + with: + name: PolyMC-${{ matrix.name }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} + path: ${{ env.INSTALL_PORTABLE_DIR }}/** + + - name: Upload binary tarball (Linux) + if: runner.os == 'Linux' uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC.tar.gz + + - name: Upload binary tarball (Linux, portable) + if: runner.os == 'Linux' + uses: actions/upload-artifact@v3 + with: + name: PolyMC-${{ runner.os }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} + path: PolyMC-portable.tar.gz + + - name: Upload AppImage (Linux) + if: runner.os == 'Linux' + uses: actions/upload-artifact@v3 + with: + name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage + path: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage + + From b0b6dd8f87e5d414045b6c7fe56fbfb128619fca Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 13 Apr 2022 19:27:41 +0200 Subject: [PATCH 267/605] fix(actions): remove macdeployqt --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab80af7e..f6790045 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -156,7 +156,6 @@ jobs: cmake --install ${{ env.BUILD_DIR }} cd ${{ env.INSTALL_DIR }} - macdeployqt "PolyMC.app" -executable="PolyMC.app/Contents/MacOS/polymc" -always-overwrite chmod +x "PolyMC.app/Contents/MacOS/polymc" tar -czf ../PolyMC.tar.gz * From abdb846c3fd9537b7eda47852ad942bc8498663f Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 16 Apr 2022 18:10:47 +0200 Subject: [PATCH 268/605] fix: set install prefix for Linux to /usr --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6790045..4b1195ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -129,7 +129,7 @@ jobs: - name: Configure CMake (Linux) if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -G Ninja ## # BUILD From c1398a6a1a28409eb918e87ab06dafd196b17c2f Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sat, 16 Apr 2022 18:30:15 +0200 Subject: [PATCH 269/605] bump to 1.2.0 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1350a3ba..ee511a76 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,8 +68,8 @@ set(Launcher_HELP_URL "https://polymc.org/wiki/help-pages/%1" CACHE STRING "URL ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 1) -set(Launcher_VERSION_MINOR 1) -set(Launcher_VERSION_HOTFIX 1) +set(Launcher_VERSION_MINOR 2) +set(Launcher_VERSION_HOTFIX 0) # Build number set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") From a549828655f39a5bf824203086c3f45b91a259c5 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sat, 16 Apr 2022 13:17:34 -0400 Subject: [PATCH 270/605] Remove the Edit menu bar menu It wouldn't bring much utility. - The keyboard shortcuts for copy/paste/etc. already work and are well-known. The menu bar likely doesn't need to advertise them. - There's not very many places you would be able to use these options in the main window (because there's not many places to type stuff in the main window). It would only be applicable on systems with a native menu bar that shows in all other windows as well (but again, the keyboard shortcuts still work). Also, rename `actionWiki` -> `actionOpenWiki` to match the corresponding `on_actionOpenWiki_triggered` --- launcher/ui/MainWindow.cpp | 61 ++++---------------------------------- 1 file changed, 6 insertions(+), 55 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 27763387..3edd185a 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -246,20 +246,12 @@ public: QMenuBar *menuBar = nullptr; QMenu *fileMenu; - QMenu *editMenu; QMenu *viewMenu; QMenu *profileMenu; QAction *actionCloseWindow; - QAction *actionUndo; - QAction *actionRedo; - QAction *actionCut; - QAction *actionCopy; - QAction *actionPaste; - QAction *actionSelectAll; - - QAction *actionWiki; + QAction *actionOpenWiki; QAction *actionNewsMenuBar; TranslatedToolbar mainToolBar; @@ -508,18 +500,7 @@ public: fileMenu->addAction(actionDeleteInstance); fileMenu->addAction(actionCopyInstance); fileMenu->addSeparator(); - - // TODO: functionality for edit actions. They're intended to be used where you can type text, e.g. notes. - editMenu = menuBar->addMenu(tr("&Edit")); - editMenu->addAction(actionUndo); - editMenu->addAction(actionRedo); - editMenu->addSeparator(); - editMenu->addAction(actionCut); - editMenu->addAction(actionCopy); - editMenu->addAction(actionPaste); - editMenu->addAction(actionSelectAll); - editMenu->addSeparator(); - editMenu->addAction(actionSettings); + fileMenu->addAction(actionSettings); viewMenu = menuBar->addMenu(tr("&View")); viewMenu->addAction(actionCAT); @@ -532,7 +513,7 @@ public: helpMenu = menuBar->addMenu(tr("&Help")); helpMenu->addAction(actionAbout); - helpMenu->addAction(actionWiki); + helpMenu->addAction(actionOpenWiki); helpMenu->addAction(actionNewsMenuBar); helpMenu->addSeparator(); if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) @@ -557,39 +538,9 @@ public: actionCloseWindow->setStatusTip(tr("Close the current window")); connect(actionCloseWindow, &QAction::triggered, APPLICATION, &Application::closeCurrentWindow); - actionUndo = new QAction(tr("&Undo"), MainWindow); - actionUndo->setShortcuts(QKeySequence::Undo); - actionUndo->setStatusTip(tr("Undo")); - actionUndo->setEnabled(false); - - actionRedo = new QAction(tr("&Redo"), MainWindow); - actionRedo->setShortcuts(QKeySequence::Redo); - actionRedo->setStatusTip(tr("Redo")); - actionRedo->setEnabled(false); - - actionCut = new QAction(tr("Cu&t"), MainWindow); - actionCut->setShortcuts(QKeySequence::Cut); - actionCut->setStatusTip(tr("Cut")); - actionCut->setEnabled(false); - - actionCopy = new QAction(tr("&Copy"), MainWindow); - actionCopy->setShortcuts(QKeySequence::Copy); - actionCopy->setStatusTip(tr("Copy")); - actionCopy->setEnabled(false); - - actionPaste = new QAction(tr("&Paste"), MainWindow); - actionPaste->setShortcuts(QKeySequence::Paste); - actionPaste->setStatusTip(tr("Paste")); - actionPaste->setEnabled(false); - - actionSelectAll = new QAction(tr("Select &All"), MainWindow); - actionSelectAll->setShortcuts(QKeySequence::SelectAll); - actionSelectAll->setStatusTip(tr("Select all")); - actionSelectAll->setEnabled(false); - - actionWiki = new QAction(tr("%1 He&lp").arg(BuildConfig.LAUNCHER_NAME), MainWindow); - actionWiki->setStatusTip(tr("Open the %1 wiki").arg(BuildConfig.LAUNCHER_NAME)); - connect(actionWiki, &QAction::triggered, MainWindow, &MainWindow::on_actionOpenWiki_triggered); + actionOpenWiki = new QAction(tr("%1 He&lp").arg(BuildConfig.LAUNCHER_NAME), MainWindow); + actionOpenWiki->setStatusTip(tr("Open the %1 wiki").arg(BuildConfig.LAUNCHER_NAME)); + connect(actionOpenWiki, &QAction::triggered, MainWindow, &MainWindow::on_actionOpenWiki_triggered); actionNewsMenuBar = new QAction(tr("&%1 &News").arg(BuildConfig.LAUNCHER_NAME), MainWindow); actionNewsMenuBar->setStatusTip(tr("Open the development blog to read more news about %1.").arg(BuildConfig.LAUNCHER_NAME)); From 9bad83a551a84e511a21791717dc930189566013 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sat, 16 Apr 2022 13:35:13 -0400 Subject: [PATCH 271/605] Use `TranslatedAction` instead of `QAction` for menu bar actions --- launcher/ui/MainWindow.cpp | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 3edd185a..528d2487 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -249,10 +249,10 @@ public: QMenu *viewMenu; QMenu *profileMenu; - QAction *actionCloseWindow; + TranslatedAction actionCloseWindow; - QAction *actionOpenWiki; - QAction *actionNewsMenuBar; + TranslatedAction actionOpenWiki; + TranslatedAction actionNewsMenuBar; TranslatedToolbar mainToolBar; TranslatedToolbar instanceToolBar; @@ -473,11 +473,10 @@ public: MainWindow->addToolBar(Qt::TopToolBarArea, mainToolBar); } - void createMenuBar(MainWindow *MainWindow) + void createMenuBar(QMainWindow *MainWindow) { menuBar = new QMenuBar(MainWindow); menuBar->setVisible(APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); - createMenuActions(MainWindow); fileMenu = menuBar->addMenu(tr("&File")); fileMenu->addAction(actionAddInstance); @@ -533,18 +532,27 @@ public: void createMenuActions(MainWindow *MainWindow) { - actionCloseWindow = new QAction(tr("Close &Window"), MainWindow); + actionCloseWindow = TranslatedAction(MainWindow); + actionCloseWindow->setObjectName(QStringLiteral("actionCloseWindow")); + actionCloseWindow.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Close &Window")); + actionCloseWindow.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Close the current window")); actionCloseWindow->setShortcut(QKeySequence::Close); - actionCloseWindow->setStatusTip(tr("Close the current window")); connect(actionCloseWindow, &QAction::triggered, APPLICATION, &Application::closeCurrentWindow); + all_actions.append(&actionCloseWindow); - actionOpenWiki = new QAction(tr("%1 He&lp").arg(BuildConfig.LAUNCHER_NAME), MainWindow); - actionOpenWiki->setStatusTip(tr("Open the %1 wiki").arg(BuildConfig.LAUNCHER_NAME)); + actionOpenWiki = TranslatedAction(MainWindow); + actionOpenWiki->setObjectName(QStringLiteral("actionOpenWiki")); + actionOpenWiki.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 He&lp")); + actionOpenWiki.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 wiki")); connect(actionOpenWiki, &QAction::triggered, MainWindow, &MainWindow::on_actionOpenWiki_triggered); + all_actions.append(&actionOpenWiki); - actionNewsMenuBar = new QAction(tr("&%1 &News").arg(BuildConfig.LAUNCHER_NAME), MainWindow); - actionNewsMenuBar->setStatusTip(tr("Open the development blog to read more news about %1.").arg(BuildConfig.LAUNCHER_NAME)); + actionNewsMenuBar = TranslatedAction(MainWindow); + actionNewsMenuBar->setObjectName(QStringLiteral("actionNewsMenuBar")); + actionNewsMenuBar.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 &News")); + actionNewsMenuBar.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 wiki")); connect(actionNewsMenuBar, &QAction::triggered, MainWindow, &MainWindow::on_actionMoreNews_triggered); + all_actions.append(&actionNewsMenuBar); } // "Instance actions" are actions that require an instance to be selected (i.e. "new instance" is not here) @@ -790,9 +798,10 @@ public: #endif createMainToolbarActions(MainWindow); + createMenuActions(dynamic_cast(MainWindow)); createInstanceActions(MainWindow); - createMenuBar(dynamic_cast(MainWindow)); + createMenuBar(MainWindow); createMainToolbar(MainWindow); From cab9afa45f7a7dfcb87080804f3a6ef90f953819 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 15 Apr 2022 12:38:27 +0200 Subject: [PATCH 272/605] fix: query for Fabric mods if Quilt is in use Right now we want to include Fabric mods in our searches where possible. Modrinth allows definining multiple loaders, while Flame only allows a single value. As a compromise we ask for Fabric mods only on Flame and for both Fabric and Quilt mods on Modrinth. --- launcher/modplatform/flame/FlameAPI.h | 11 ++++++++++- launcher/modplatform/flame/FlameModIndex.cpp | 6 ++++-- launcher/modplatform/modrinth/ModrinthAPI.h | 3 +++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 9bcc357e..ce02df65 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -23,7 +23,7 @@ class FlameAPI : public NetworkModAPI { .arg(args.offset) .arg(args.search) .arg(args.sorting) - .arg(args.mod_loader) + .arg(getMappedModLoader(args.mod_loader)) .arg(gameVersionStr); }; @@ -31,4 +31,13 @@ class FlameAPI : public NetworkModAPI { { return QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(args.addonId); }; + + public: + static auto getMappedModLoader(const ModLoaderType type) -> const ModLoaderType + { + // TODO: remove this once Quilt drops official Fabric support + if (type == Quilt) // NOTE: Most if not all Fabric mods should work *currently* + return Fabric; + return type; + } }; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index e86b64dd..c7b86b5c 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -3,6 +3,7 @@ #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "modplatform/flame/FlameAPI.h" #include "net/NetJob.h" void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) @@ -43,8 +44,9 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, BaseInstance* inst) { QVector unsortedVersions; - bool hasFabric = !(dynamic_cast(inst))->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader").isEmpty(); - QString mcVersion = (dynamic_cast(inst))->getPackProfile()->getComponentVersion("net.minecraft"); + auto profile = (dynamic_cast(inst))->getPackProfile(); + bool hasFabric = FlameAPI::getMappedModLoader(profile->getModLoader()) == ModAPI::Fabric; + QString mcVersion = profile->getComponentVersion("net.minecraft"); for (auto versionIter : arr) { auto obj = versionIter.toObject(); diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 6604d772..84dd7d03 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -54,6 +54,9 @@ class ModrinthAPI : public NetworkModAPI { { if (type == Unspecified) return "fabric, forge, quilt"; + // TODO: remove this once Quilt drops official Fabric support + if (type == Quilt) // NOTE: Most if not all Fabric mods should work *currently* + return "fabric, quilt"; return ModAPI::getModLoaderString(type); } From 0c581cfb62aee1b1a976b2acd005de289b468dc3 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sun, 17 Apr 2022 09:53:30 +0200 Subject: [PATCH 273/605] CHANGE: use Qt 5.15.3 (from brew) on macos More updated Qt means less bugs and generally less issues. The only drawback is losing MacOS Sierra support --- .github/workflows/build.yml | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4b1195ee..196b6d79 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,9 +26,7 @@ jobs: msystem: mingw64 - os: macos-11 - qt_version: 5.12.12 - qt_host: mac - macosx_deployment_target: 10.12 + macosx_deployment_target: 10.13 runs-on: ${{ matrix.os }} @@ -74,23 +72,11 @@ jobs: distribution: 'temurin' java-version: '17' - - name: Cache Qt (macOS) - if: runner.os == 'macOS' - id: cache-qt - uses: actions/cache@v3 - with: - path: "${{ github.workspace }}/Qt/" - key: ${{ runner.os }}-${{ matrix.qt_version }}-${{ matrix.qt_arch }}-qt_cache - - name: Install Qt (macOS) if: runner.os == 'macOS' - uses: jurplel/install-qt-action@v2 - with: - version: ${{ matrix.qt_version }} - host: ${{ matrix.qt_host }} - arch: ${{ matrix.qt_arch }} - cached: ${{ steps.cache-qt.outputs.cache-hit }} - dir: "${{ github.workspace }}/Qt/" + run: | + brew update + brew install qt@5 - name: Install Qt (Linux) if: runner.os == 'Linux' @@ -118,7 +104,7 @@ jobs: - name: Configure CMake (macOS) if: runner.os == 'macOS' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DQt5_DIR=/usr/local/opt/qt@5 -DCMAKE_PREFIX_PATH=/usr/local/opt/qt@5 -G Ninja - name: Configure CMake (Windows) if: runner.os == 'Windows' From 3acc7614194d4e164bebc1e97c818a99ecdb2607 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 17 Apr 2022 12:44:24 -0400 Subject: [PATCH 274/605] Fix bugs with instance menu bar options when opening without instances - The launch option is no longer empty. - The program now checks on startup whether an instance is selected to decide whether to disable instance options. Also, get rid of a dynamic cast. --- launcher/ui/MainWindow.cpp | 40 ++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 528d2487..2b219aff 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -636,6 +636,8 @@ public: actionLaunchInstance = TranslatedAction(MainWindow); actionLaunchInstance->setObjectName(QStringLiteral("actionLaunchInstance")); + actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Launch")); + actionLaunchInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance.")); all_actions.append(&actionLaunchInstance); actionLaunchInstanceOffline = TranslatedAction(MainWindow); @@ -734,6 +736,7 @@ public: actionCopyInstance->setShortcut(QKeySequence(tr("Ctrl+D"))); all_actions.append(&actionCopyInstance); + setInstanceActionsEnabled(false); } void createInstanceToolbar(QMainWindow *MainWindow) @@ -784,7 +787,7 @@ public: MainWindow->addToolBar(Qt::RightToolBarArea, instanceToolBar); } - void setupUi(QMainWindow *MainWindow) + void setupUi(MainWindow *MainWindow) { if (MainWindow->objectName().isEmpty()) { @@ -798,7 +801,7 @@ public: #endif createMainToolbarActions(MainWindow); - createMenuActions(dynamic_cast(MainWindow)); + createMenuActions(MainWindow); createInstanceActions(MainWindow); createMenuBar(MainWindow); @@ -818,6 +821,8 @@ public: createNewsToolbar(MainWindow); createInstanceToolbar(MainWindow); + MainWindow->updateToolsMenu(); + retranslateUi(MainWindow); QMetaObject::connectSlotsByName(MainWindow); @@ -1146,7 +1151,7 @@ void MainWindow::updateToolsMenu() QToolButton *launchButton = dynamic_cast(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstance)); QToolButton *launchOfflineButton = dynamic_cast(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstanceOffline)); - if(!m_selectedInstance || m_selectedInstance->isRunning()) + if(m_selectedInstance && m_selectedInstance->isRunning()) { ui->actionLaunchInstance->setMenu(nullptr); ui->actionLaunchInstanceOffline->setMenu(nullptr); @@ -1179,14 +1184,20 @@ void MainWindow::updateToolsMenu() normalLaunch->setShortcut(QKeySequence::Open); QAction *normalLaunchOffline = launchOfflineMenu->addAction(tr("Launch Offline")); normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); - connect(normalLaunch, &QAction::triggered, [this]() - { - APPLICATION->launch(m_selectedInstance, true); - }); - connect(normalLaunchOffline, &QAction::triggered, [this]() - { - APPLICATION->launch(m_selectedInstance, false); - }); + if (m_selectedInstance) + { + connect(normalLaunch, &QAction::triggered, [this]() { + APPLICATION->launch(m_selectedInstance, true); + }); + connect(normalLaunchOffline, &QAction::triggered, [this]() { + APPLICATION->launch(m_selectedInstance, false); + }); + } + else + { + normalLaunch->setDisabled(true); + normalLaunchOffline->setDisabled(true); + } QString profilersTitle = tr("Profilers"); launchMenu->addSeparator()->setText(profilersTitle); launchOfflineMenu->addSeparator()->setText(profilersTitle); @@ -1203,7 +1214,7 @@ void MainWindow::updateToolsMenu() profilerAction->setToolTip(profilerToolTip); profilerOfflineAction->setToolTip(profilerToolTip); } - else + else if (m_selectedInstance) { connect(profilerAction, &QAction::triggered, [this, profiler]() { @@ -1214,6 +1225,11 @@ void MainWindow::updateToolsMenu() APPLICATION->launch(m_selectedInstance, false, profiler.get()); }); } + else + { + profilerAction->setDisabled(true); + profilerOfflineAction->setDisabled(true); + } } ui->actionLaunchInstance->setMenu(launchMenu); ui->actionLaunchInstanceOffline->setMenu(launchOfflineMenu); From 6b45386252caa03cecf0e18777691e4895770e5c Mon Sep 17 00:00:00 2001 From: Kenneth Chew <79120643+kthchew@users.noreply.github.com> Date: Sun, 17 Apr 2022 20:32:51 +0000 Subject: [PATCH 275/605] Disable instead of hide menu bar option on Linux Co-authored-by: Sefa Eyeoglu --- launcher/ui/pages/global/LauncherPage.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index a213eff0..097a2bfa 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -414,7 +414,10 @@ void LauncherPage::loadSettings() } // 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()); // Console settings From 4c52cc414f87c9d94a12412045ddfa3cb82f75b5 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 17 Apr 2022 16:34:41 -0400 Subject: [PATCH 276/605] Improve menu bar setting string --- launcher/ui/pages/global/LauncherPage.ui | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 636aec15..4cc2a113 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -330,8 +330,11 @@ + + The menubar is more friendly for keyboard-driven interaction. + - Always show menu bar instead of tool bar (more keyboard friendly, less pretty) + Replace toolbar with menubar From fcbf37f60fd872884a59fc11233e87e47029e1c9 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 17 Apr 2022 17:58:51 -0400 Subject: [PATCH 277/605] Fix typos and inconsistent capitalization in sort options --- .../pages/modplatform/atlauncher/AtlFilterModel.cpp | 6 +++--- launcher/ui/pages/modplatform/flame/FlameModPage.cpp | 2 +- launcher/ui/pages/modplatform/flame/FlamePage.cpp | 12 ++++++------ launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp | 6 +++--- .../ui/pages/modplatform/legacy_ftb/ListModel.cpp | 4 ++-- .../ui/pages/modplatform/modrinth/ModrinthPage.cpp | 8 ++++---- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp index 3a97d477..c1ab166b 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -27,9 +27,9 @@ namespace Atl { FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) { currentSorting = Sorting::ByPopularity; - sortings.insert(tr("Sort by popularity"), Sorting::ByPopularity); - sortings.insert(tr("Sort by name"), Sorting::ByName); - sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion); + sortings.insert(tr("Sort by Popularity"), Sorting::ByPopularity); + sortings.insert(tr("Sort by Name"), Sorting::ByName); + sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); searchTerm = ""; } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 864ae8e6..203adb3b 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -48,7 +48,7 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) // index is used to set the sorting with the flame 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 Last Updated")); ui->sortByBox->addItem(tr("Sort by Name")); ui->sortByBox->addItem(tr("Sort by Author")); ui->sortByBox->addItem(tr("Sort by Downloads")); diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index cbe709c2..c90294ce 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -57,12 +57,12 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget *parent) ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); // 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")); + 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); diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp index 67e2277c..cbf347fc 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp @@ -26,9 +26,9 @@ namespace Ftb { FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) { currentSorting = Sorting::ByPlays; - sortings.insert(tr("Sort by plays"), Sorting::ByPlays); - sortings.insert(tr("Sort by installs"), Sorting::ByInstalls); - sortings.insert(tr("Sort by name"), Sorting::ByName); + sortings.insert(tr("Sort by Plays"), Sorting::ByPlays); + sortings.insert(tr("Sort by Installs"), Sorting::ByInstalls); + sortings.insert(tr("Sort by Name"), Sorting::ByName); } const QMap FilterModel::getAvailableSortings() diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 9c46e887..63b944c4 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -16,8 +16,8 @@ namespace LegacyFTB { FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) { currentSorting = Sorting::ByGameVersion; - sortings.insert(tr("Sort by name"), Sorting::ByName); - sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion); + sortings.insert(tr("Sort by Name"), Sorting::ByName); + sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); } bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index ddaf96e2..12aee51b 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -46,14 +46,14 @@ ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) ui->packView->setModel(listModel); // index is used to set the sorting with the modrinth api - ui->sortByBox->addItem(tr("Sort by Relevence")); + ui->sortByBox->addItem(tr("Sort by Relevance")); ui->sortByBox->addItem(tr("Sort by Downloads")); ui->sortByBox->addItem(tr("Sort by Follows")); - ui->sortByBox->addItem(tr("Sort by last updated")); - ui->sortByBox->addItem(tr("Sort by newest")); + ui->sortByBox->addItem(tr("Sort by Last Updated")); + ui->sortByBox->addItem(tr("Sort by Newest")); // 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... + // so it's best not to connect them in the parent's constructor... connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); From cbbcc2d68bcc262cb6a4c75947456d85b6774e52 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 17 Apr 2022 19:24:49 -0300 Subject: [PATCH 278/605] fix(translation): don't translate placeholders Those are modified programatically, and never show up to the user! --- launcher/ui/widgets/ModFilterWidget.ui | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/ui/widgets/ModFilterWidget.ui b/launcher/ui/widgets/ModFilterWidget.ui index ad1090e2..ebe5d2be 100644 --- a/launcher/ui/widgets/ModFilterWidget.ui +++ b/launcher/ui/widgets/ModFilterWidget.ui @@ -26,21 +26,21 @@ - allVersions + allVersions - strictVersion + strictVersion - majorVersion + majorVersion From 1bb35b9204b6a6830610acd99090a4f58706dcab Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Mon, 18 Apr 2022 12:09:24 +0200 Subject: [PATCH 279/605] specify -DLauncher_BUILD_PLATFORM on CI builds more cool also maybe helps with updater? --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 196b6d79..c22baed3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -104,18 +104,18 @@ jobs: - name: Configure CMake (macOS) if: runner.os == 'macOS' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DQt5_DIR=/usr/local/opt/qt@5 -DCMAKE_PREFIX_PATH=/usr/local/opt/qt@5 -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DQt5_DIR=/usr/local/opt/qt@5 -DCMAKE_PREFIX_PATH=/usr/local/opt/qt@5 -DLauncher_BUILD_PLATFORM=macOS -G Ninja - name: Configure CMake (Windows) if: runner.os == 'Windows' shell: msys2 {0} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -G Ninja - name: Configure CMake (Linux) if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -G Ninja ## # BUILD From fa352ff4d379e3b29ea73d53ba458689c634b467 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 18 Apr 2022 14:15:02 +0200 Subject: [PATCH 280/605] fix: actually check if a mod loader is selected Thus also removes a suggestCurrent call from loaderFilterChanged, as it will already be triggered by setSelectedLoaderVersion --- launcher/ui/pages/modplatform/VanillaPage.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/VanillaPage.cpp index 207d0130..175fda7d 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.cpp +++ b/launcher/ui/pages/modplatform/VanillaPage.cpp @@ -158,7 +158,6 @@ void VanillaPage::loaderFilterChanged() auto vlist = APPLICATION->metadataIndex()->get(m_selectedLoader); ui->loaderVersionList->initialize(vlist.get()); ui->loaderVersionList->selectRecommended(); - suggestCurrent(); ui->loaderVersionList->setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion)); } @@ -205,8 +204,8 @@ void VanillaPage::suggestCurrent() return; } - // List is empty if either no mod loader is selected, or no versions are available - if(!ui->loaderVersionList->hasVersions()) + // There isn't a selected version if the version list is empty + if(ui->loaderVersionList->selectedVersion() == nullptr) dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion)); else { From ac77997a7afbdd6420232109c213e47ee5b0dc24 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 18 Apr 2022 14:36:36 +0200 Subject: [PATCH 281/605] fix: handle network errors when downloading modlist --- launcher/ui/pages/modplatform/ModModel.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index da0331b5..e82e1cdb 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -192,13 +192,14 @@ void ListModel::searchRequestFinished(QJsonDocument& doc) void ListModel::searchRequestFailed(QString reason) { - if (jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { + if (!jobPtr->first()->m_reply) { + // Network error + QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods.")); + } else if (jobPtr->first()->m_reply && jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { // 409 Gone, notify user to update QMessageBox::critical(nullptr, tr("Error"), //: %1 refers to the launcher itself QString("%1 %2").arg(m_parent->displayName()).arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME))); - // self-destruct - (dynamic_cast((dynamic_cast(parent()))->parentWidget()))->reject(); } jobPtr.reset(); From c174a1eb0189cb182769245effe530550c63c499 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 18 Apr 2022 15:05:41 +0200 Subject: [PATCH 282/605] fix: don't set mod loader as important --- launcher/InstanceCreationTask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp index 24bc5f46..e01bf306 100644 --- a/launcher/InstanceCreationTask.cpp +++ b/launcher/InstanceCreationTask.cpp @@ -31,7 +31,7 @@ void InstanceCreationTask::executeTask() components->buildingFromScratch(); components->setComponentVersion("net.minecraft", m_version->descriptor(), true); if(m_usingLoader) - components->setComponentVersion(m_loader, m_loaderVersion->descriptor(), true); + components->setComponentVersion(m_loader, m_loaderVersion->descriptor()); inst.setName(m_instName); inst.setIconKey(m_instIcon); instanceSettings->resumeSave(); From 7b9d462fbcf1759b126a8852cd2070ac9afba471 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Mon, 18 Apr 2022 18:12:59 +0200 Subject: [PATCH 283/605] remove "on x" --- launcher/ui/MainWindow.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 2b219aff..7ac4d2d4 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -831,10 +831,6 @@ public: void retranslateUi(QMainWindow *MainWindow) { QString winTitle = tr("%1 - Version %2", "Launcher - Version X").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()); - if (!BuildConfig.BUILD_PLATFORM.isEmpty()) - { - winTitle += tr(" on %1", "on platform, as in operating system").arg(BuildConfig.BUILD_PLATFORM); - } MainWindow->setWindowTitle(winTitle); // all the actions for(auto * item: all_actions) From ebded1ec4929e7037da6d0dad488d5cc1cc512ca Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 18 Apr 2022 13:56:32 -0400 Subject: [PATCH 284/605] Fix formatting of version string on macOS --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d726d9c8..356924c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -183,9 +183,9 @@ if(UNIX AND APPLE) set(MACOSX_BUNDLE_BUNDLE_NAME "${Launcher_Name}") set(MACOSX_BUNDLE_INFO_STRING "${Launcher_Name}: A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.") set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.polymc.${Launcher_Name}") - set(MACOSX_BUNDLE_BUNDLE_VERSION "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}") - set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}") - set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}") + set(MACOSX_BUNDLE_BUNDLE_VERSION "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}") + set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}") + set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}") set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns) set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2021-2022 ${Launcher_Copyright}") From fcdc7a1a358ba9d8dcf0492310dd3528dc05a911 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 18 Apr 2022 13:12:42 +0200 Subject: [PATCH 285/605] fix: fix Modrinth query when Quilt is in use --- launcher/modplatform/modrinth/ModrinthAPI.h | 49 +++++++++++++------ launcher/ui/pages/modplatform/ModPage.cpp | 6 +-- launcher/ui/pages/modplatform/ModPage.h | 2 +- .../pages/modplatform/flame/FlameModPage.cpp | 4 +- .../ui/pages/modplatform/flame/FlameModPage.h | 3 +- .../modplatform/modrinth/ModrinthPage.cpp | 15 +++++- .../pages/modplatform/modrinth/ModrinthPage.h | 3 +- 7 files changed, 57 insertions(+), 25 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 84dd7d03..86852c94 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -1,5 +1,6 @@ #pragma once +#include "modplatform/ModAPI.h" #include "modplatform/helpers/NetworkModAPI.h" #include @@ -8,6 +9,36 @@ class ModrinthAPI : public NetworkModAPI { public: inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; + static auto getModLoaderStrings(ModLoaderType type) -> const QStringList + { + QStringList l; + switch (type) + { + case Unspecified: + for (auto loader : {Forge, Fabric, Quilt}) + { + l << ModAPI::getModLoaderString(loader); + } + break; + + case Quilt: + l << ModAPI::getModLoaderString(Fabric); + default: + l << ModAPI::getModLoaderString(type); + } + return l; + } + + static auto getModLoaderFilters(ModLoaderType type) -> const QString + { + QStringList l; + for (auto loader : getModLoaderStrings(type)) + { + l << QString("\"categories:%1\"").arg(loader); + } + return l.join(','); + } + private: inline auto getModSearchURL(SearchArgs& args) const -> QString override { @@ -22,11 +53,11 @@ class ModrinthAPI : public NetworkModAPI { "limit=25&" "query=%2&" "index=%3&" - "facets=[[\"categories:%4\"],%5[\"project_type:mod\"]]") + "facets=[[%4],%5[\"project_type:mod\"]]") .arg(args.offset) .arg(args.search) .arg(args.sorting) - .arg(getModLoaderString(args.mod_loader)) + .arg(getModLoaderFilters(args.mod_loader)) .arg(getGameVersionsArray(args.versions)); }; @@ -34,10 +65,10 @@ class ModrinthAPI : public NetworkModAPI { { return QString("https://api.modrinth.com/v2/project/%1/version?" "game_versions=[%2]" - "loaders=[%3]") + "loaders=[\"%3\"]") .arg(args.addonId) .arg(getGameVersionsString(args.mcVersions)) - .arg(getModLoaderString(args.loader)); + .arg(getModLoaderStrings(args.loader).join("\",\"")); }; auto getGameVersionsArray(std::list mcVersions) const -> QString @@ -50,16 +81,6 @@ class ModrinthAPI : public NetworkModAPI { return s.isEmpty() ? QString() : QString("[%1],").arg(s); } - static auto getModLoaderString(ModLoaderType type) -> const QString - { - if (type == Unspecified) - return "fabric, forge, quilt"; - // TODO: remove this once Quilt drops official Fabric support - if (type == Quilt) // NOTE: Most if not all Fabric mods should work *currently* - return "fabric, quilt"; - return ModAPI::getModLoaderString(type); - } - inline auto validateModLoader(ModLoaderType modLoader) const -> bool { return modLoader == Unspecified || modLoader == Forge || modLoader == Fabric || modLoader == Quilt; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 29f6b601..6dd3a453 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -170,14 +170,12 @@ void ModPage::updateModVersions(int prev_count) QString mcVersion = packProfile->getComponentVersion("net.minecraft"); - QString loaderString = ModAPI::getModLoaderString(packProfile->getModLoader()); - for (int i = 0; i < current.versions.size(); i++) { auto version = current.versions[i]; bool valid = false; for(auto& mcVer : m_filter->versions){ - //NOTE: Flame doesn't care about loaderString, so passing it changes nothing. - if (validateVersion(version, mcVer.toString(), loaderString)) { + //NOTE: Flame doesn't care about loader, so passing it changes nothing. + if (validateVersion(version, mcVer.toString(), packProfile->getModLoader())) { valid = true; break; } diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 85aaede9..eb89b0e2 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -37,7 +37,7 @@ class ModPage : public QWidget, public BasePage { void retranslate() override; auto shouldDisplay() const -> bool override = 0; - virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool = 0; + virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool = 0; auto apiProvider() const -> const ModAPI* { return api.get(); }; auto getFilter() const -> const std::shared_ptr { return m_filter; } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 864ae8e6..9afbdb52 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -61,9 +61,9 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected); } -auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool +auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool { - (void) loaderVer; + Q_UNUSED(loader); return ver.mcVersion.contains(mineVer); } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index d96a0720..27cbdb8c 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -35,6 +35,7 @@ #pragma once +#include "modplatform/ModAPI.h" #include "ui/pages/modplatform/ModPage.h" #include "modplatform/flame/FlameAPI.h" @@ -54,7 +55,7 @@ class FlameModPage : public ModPage { inline auto debugName() const -> QString override { return "Flame"; } inline auto metaEntryBase() const -> QString override { return "FlameMods"; }; - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool override; auto shouldDisplay() const -> bool override; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index ddaf96e2..f0e73c51 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -34,6 +34,7 @@ */ #include "ModrinthPage.h" +#include "modplatform/modrinth/ModrinthAPI.h" #include "ui_ModPage.h" #include "ModrinthModel.h" @@ -60,9 +61,19 @@ ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthPage::onModSelected); } -auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool +auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool { - return ver.mcVersion.contains(mineVer) && ver.loaders.contains(loaderVer); + auto loaderStrings = ModrinthAPI::getModLoaderStrings(loader); + + auto loaderCompatible = false; + for (auto remoteLoader : ver.loaders) + { + if (loaderStrings.contains(remoteLoader)) { + loaderCompatible = true; + break; + } + } + return ver.mcVersion.contains(mineVer) && loaderCompatible; } // I don't know why, but doing this on the parent class makes it so that diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 0bde43eb..e3a0e1f0 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -35,6 +35,7 @@ #pragma once +#include "modplatform/ModAPI.h" #include "ui/pages/modplatform/ModPage.h" #include "modplatform/modrinth/ModrinthAPI.h" @@ -54,7 +55,7 @@ class ModrinthPage : public ModPage { inline auto debugName() const -> QString override { return "Modrinth"; } inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; }; - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool override; auto shouldDisplay() const -> bool override; }; From 27c72935f8a17761e9b18a74643e014cad3587d1 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 19 Apr 2022 15:07:14 +0200 Subject: [PATCH 286/605] fix: use size in bytes to sort by world size --- launcher/minecraft/WorldList.cpp | 13 ++++++++++++- launcher/ui/pages/instance/WorldListPage.cpp | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index 344bea63..955609bf 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -17,6 +17,7 @@ #include "Application.h" #include +#include #include #include #include @@ -190,6 +191,16 @@ QVariant WorldList::data(const QModelIndex &index, int role) const return QVariant(); } + case Qt::UserRole: + switch (column) + { + case SizeColumn: + return qVariantFromValue(world.bytes()); + + default: + return data(index, Qt::DisplayRole); + } + case Qt::ToolTipRole: { return world.folderName(); @@ -216,7 +227,7 @@ QVariant WorldList::data(const QModelIndex &index, int role) const } case SizeRole: { - return locale.formattedDataSize(world.bytes()); + return qVariantFromValue(world.bytes()); } case IconFileRole: { diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 650583a2..76725539 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include "tools/MCEditTool.h" #include "FileSystem.h" @@ -92,6 +93,7 @@ WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr worl WorldListProxyModel * proxy = new WorldListProxyModel(this); proxy->setSortCaseSensitivity(Qt::CaseInsensitive); proxy->setSourceModel(m_worlds.get()); + proxy->setSortRole(Qt::UserRole); ui->worldTreeView->setSortingEnabled(true); ui->worldTreeView->setModel(proxy); ui->worldTreeView->installEventFilter(this); From ec2ac2e80cce0c764f8099105f1e07e95cdfca8a Mon Sep 17 00:00:00 2001 From: txtsd Date: Tue, 19 Apr 2022 15:36:15 +0530 Subject: [PATCH 287/605] fix: Only trigger rename on KeyPress This is macOS specific --- launcher/ui/instanceview/InstanceDelegate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/instanceview/InstanceDelegate.cpp b/launcher/ui/instanceview/InstanceDelegate.cpp index 22ff78cd..b446e39d 100644 --- a/launcher/ui/instanceview/InstanceDelegate.cpp +++ b/launcher/ui/instanceview/InstanceDelegate.cpp @@ -364,7 +364,7 @@ public: { QKeyEvent *keyEvent = static_cast(event); auto key = keyEvent->key(); - if (key == Qt::Key_Return || key == Qt::Key_Enter) + if ((key == Qt::Key_Return || key == Qt::Key_Enter) && eventType == QEvent::KeyPress) { emit editingDone(); return true; From 53ff66c3173257cec84d5a0c314957122d1173a7 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 18 Apr 2022 00:41:43 +0200 Subject: [PATCH 288/605] fix: update files for relase workflow --- .github/workflows/trigger_release.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index 149cb632..cea7bc6c 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -33,6 +33,7 @@ jobs: - name: Package artifacts properly run: | mv ${{ github.workspace }}/PolyMC-source PolyMC-${{ env.VERSION }} + mv PolyMC-Linux-Portable*/PolyMC-portable.tar.gz PolyMC-Linux-Portable-{{env.VERSION}}.tar.gz mv PolyMC-Linux*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz @@ -42,9 +43,9 @@ jobs: for d in PolyMC-Windows-*; do cd "${d}" || continue ARCH="$(echo -n ${d} | cut -d '-' -f 3)" - PORT="$(echo -n ${d} | grep -o portable || true)" + PORT="$(echo -n ${d} | grep -o Portable || true)" NAME="PolyMC-Windows-${ARCH}" - test -z "${PORT}" || NAME="${NAME}-portable" + test -z "${PORT}" || NAME="${NAME}-Portable" zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * cd .. done @@ -61,10 +62,11 @@ jobs: prerelease: false files: | PolyMC-Linux-${{ env.VERSION }}.tar.gz + PolyMC-Linux-Portable-${{ env.VERSION }}.tar.gz PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage PolyMC-Windows-i686-${{ env.VERSION }}.zip - PolyMC-Windows-i686-portable-${{ env.VERSION }}.zip + PolyMC-Windows-i686-Portable-${{ env.VERSION }}.zip PolyMC-Windows-x86_64-${{ env.VERSION }}.zip - PolyMC-Windows-x86_64-portable-${{ env.VERSION }}.zip + PolyMC-Windows-x86_64-Portable-${{ env.VERSION }}.zip PolyMC-macOS-${{ env.VERSION }}.tar.gz PolyMC-${{ env.VERSION }}.tar.gz From c3524a9d5706f09502f3d69175b867920248f2ba Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 18 Apr 2022 12:45:25 +0200 Subject: [PATCH 289/605] fix: bundle binary tarball as user root --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c22baed3..3f2c07a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -171,7 +171,7 @@ jobs: cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_DIR }} cd ${{ env.INSTALL_DIR }} - tar -czf ../PolyMC.tar.gz * + tar --owner root --group root -czf ../PolyMC.tar.gz * - name: Package (Linux, portable) if: runner.os == 'Linux' From 9462dd3ddc55e3e48e47ca794c05e11cabb6226e Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 17 Apr 2022 21:52:46 -0400 Subject: [PATCH 290/605] Improve security by enabling hardened runtime for macOS This change also fixes a bug on recent versions of macOS where Minecraft mods that requested access to the microphone would silently fail. --- .github/workflows/build.yml | 3 ++- cmake/MacOSXBundleInfo.plist.in | 4 ++++ program_info/App.entitlements | 12 ++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 program_info/App.entitlements diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c22baed3..d41e898f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -115,7 +115,7 @@ jobs: - name: Configure CMake (Linux) if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -G Ninja ## # BUILD @@ -143,6 +143,7 @@ jobs: cd ${{ env.INSTALL_DIR }} chmod +x "PolyMC.app/Contents/MacOS/polymc" + sudo codesign --sign - --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PolyMC.app/Contents/MacOS/polymc" tar -czf ../PolyMC.tar.gz * - name: Package (Windows) diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index 050123ee..0e3a43c6 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -2,6 +2,10 @@ + NSCameraUsageDescription + A Minecraft mod wants to access your camera. + NSMicrophoneUsageDescription + A Minecraft mod wants to access your microphone. NSPrincipalClass NSApplication NSHighResolutionCapable diff --git a/program_info/App.entitlements b/program_info/App.entitlements new file mode 100644 index 00000000..1850b990 --- /dev/null +++ b/program_info/App.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.disable-library-validation + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + + From c637e3657ce5dfa72f39df52a18c31b0c7acf63d Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Tue, 19 Apr 2022 18:07:42 +0200 Subject: [PATCH 291/605] bump to 1.2.1 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 356924c3..a7824a99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,7 +69,7 @@ set(Launcher_HELP_URL "https://polymc.org/wiki/help-pages/%1" CACHE STRING "URL ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 1) set(Launcher_VERSION_MINOR 2) -set(Launcher_VERSION_HOTFIX 0) +set(Launcher_VERSION_HOTFIX 1) # Build number set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") From 4a3d94aaf9092a5f4b53690568a29597a41a3619 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 19 Apr 2022 19:06:17 +0200 Subject: [PATCH 292/605] fix: fix filename of linux portable --- .github/workflows/trigger_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index cea7bc6c..ff0d2b3f 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -33,7 +33,7 @@ jobs: - name: Package artifacts properly run: | mv ${{ github.workspace }}/PolyMC-source PolyMC-${{ env.VERSION }} - mv PolyMC-Linux-Portable*/PolyMC-portable.tar.gz PolyMC-Linux-Portable-{{env.VERSION}}.tar.gz + mv PolyMC-Linux-Portable*/PolyMC-portable.tar.gz PolyMC-Linux-Portable-${{ env.VERSION }}.tar.gz mv PolyMC-Linux*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz From 4c5f701b05dfd5fb48bb3d41b59d6e3ad8bc63cf Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 19 Apr 2022 21:49:54 +0200 Subject: [PATCH 293/605] Revert "better FreeBSD support" --- launcher/minecraft/MinecraftInstance.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 503b34d9..3ba79178 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -213,12 +213,8 @@ QString MinecraftInstance::binRoot() const QString MinecraftInstance::getNativePath() const { -#ifdef Q_OS_FREEBSD - QDir natives_dir("/usr/local/lib/lwjgl/"); -#else QDir natives_dir(FS::PathCombine(instanceRoot(), "natives/")); return natives_dir.absolutePath(); -#endif } QString MinecraftInstance::getLocalLibraryPath() const From 0682fe544a52a9d018bb05ddcacf4c697a826094 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Tue, 19 Apr 2022 22:20:00 -0400 Subject: [PATCH 294/605] Fix crash if no Minecraft version is selected in the new instance screen --- launcher/ui/pages/modplatform/VanillaPage.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/VanillaPage.cpp b/launcher/ui/pages/modplatform/VanillaPage.cpp index 175fda7d..a026947f 100644 --- a/launcher/ui/pages/modplatform/VanillaPage.cpp +++ b/launcher/ui/pages/modplatform/VanillaPage.cpp @@ -118,7 +118,18 @@ void VanillaPage::filterChanged() void VanillaPage::loaderFilterChanged() { - auto minecraftVersion = m_selectedVersion->descriptor(); + QString minecraftVersion; + if (m_selectedVersion) + { + minecraftVersion = m_selectedVersion->descriptor(); + } + else + { + ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // empty list + ui->loaderVersionList->setEmptyString(tr("No Minecraft version is selected.")); + ui->loaderVersionList->setEmptyMode(VersionListView::String); + return; + } if(ui->noneFilter->isChecked()) { ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // empty list From db6dae754135a4e05420489571ad1233d0ec8933 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 20 Apr 2022 09:43:25 +0200 Subject: [PATCH 295/605] fix: disable major version match for snapshots --- launcher/ui/widgets/ModFilterWidget.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index ffc8d05d..4ab34375 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -23,12 +23,21 @@ void ModFilterWidget::setInstance(MinecraftInstance* instance) { m_instance = instance; - auto mcVersionSplit = mcVersionStr().split("."); - ui->strictVersionButton->setText( tr("Strict match (= %1)").arg(mcVersionStr())); - ui->majorVersionButton->setText( - tr("Major version match (= %1.%2.x)").arg(mcVersionSplit[0], mcVersionSplit[1])); + + // we can't do this for snapshots sadly + if(mcVersionStr().contains('.')) + { + auto mcVersionSplit = mcVersionStr().split("."); + ui->majorVersionButton->setText( + tr("Major version match (= %1.%2.x)").arg(mcVersionSplit[0], mcVersionSplit[1])); + } + else + { + ui->majorVersionButton->setText(tr("Major version match (unsupported)")); + disableVersionButton(Major); + } ui->allVersionsButton->setText( tr("Any version")); //ui->betweenVersionsButton->setText( @@ -67,7 +76,6 @@ void ModFilterWidget::onVersionFilterChanged(int id) //ui->lowerVersionComboBox->setEnabled(id == VersionButtonID::Between); //ui->upperVersionComboBox->setEnabled(id == VersionButtonID::Between); - auto versionSplit = mcVersionStr().split("."); int index = 0; auto cast_id = (VersionButtonID) id; @@ -83,12 +91,14 @@ void ModFilterWidget::onVersionFilterChanged(int id) case(VersionButtonID::Strict): m_filter->versions.push_front(mcVersion()); break; - case(VersionButtonID::Major): + case(VersionButtonID::Major): { + auto versionSplit = mcVersionStr().split("."); for(auto i = Version(QString("%1.%2").arg(versionSplit[0], versionSplit[1])); i <= mcVersion(); index++){ m_filter->versions.push_front(i); i = Version(QString("%1.%2.%3").arg(versionSplit[0], versionSplit[1], QString("%1").arg(index))); } break; + } case(VersionButtonID::All): // Empty list to avoid enumerating all versions :P break; From b3e1691c017d8d791f2f66411fc634e6bfc330d0 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 20 Apr 2022 18:33:33 +0200 Subject: [PATCH 296/605] fix: hide LauncherLoginStep tokens for non-Debug builds --- launcher/minecraft/auth/steps/LauncherLoginStep.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp index c978bd07..f5697223 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -50,7 +50,9 @@ void LauncherLoginStep::onRequestDone( auto requestor = qobject_cast(QObject::sender()); requestor->deleteLater(); +#ifndef NDEBUG qDebug() << data; +#endif if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; #ifndef NDEBUG From 3ec511010fbff31c392090548396080e44b8389c Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 21 Apr 2022 09:34:44 -0300 Subject: [PATCH 297/605] fix: Build iconfix as static library On CI we build using the bundled Quazip, and automatically set -DBUILD_STATIC_LIBS to true, so it build iconfix statically as well. However, since we recently added support for using the system quazip, this flag is not set anymore, and PolyMC fails to run because iconfix neither is statically linked, nor it creates a .so file for dynamic linking. Since most other libs are built statically, we should make this one static as well. Maybe we should consider allowing for dynamic linking of libs now that quazip is not much of an issue anymore. :^) --- libraries/iconfix/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/iconfix/CMakeLists.txt b/libraries/iconfix/CMakeLists.txt index 08441203..97a59129 100644 --- a/libraries/iconfix/CMakeLists.txt +++ b/libraries/iconfix/CMakeLists.txt @@ -12,7 +12,7 @@ internal/qiconloader.cpp internal/qiconloader_p.h ) -add_library(Launcher_iconfix ${ICONFIX_SOURCES}) +add_library(Launcher_iconfix STATIC ${ICONFIX_SOURCES}) target_include_directories(Launcher_iconfix PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} "${CMAKE_CURRENT_BINARY_DIR}" ) target_link_libraries(Launcher_iconfix Qt5::Core Qt5::Widgets) From 908e6364c9c156f53146df9d32506ace15bd0360 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 21 Apr 2022 22:52:05 +0200 Subject: [PATCH 298/605] fix: update README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4c73c47e..c493293d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ PolyMC is a custom launcher for Minecraft that focuses on predictability, long term stability and simplicity. -This is a **fork** of the MultiMC Launcher and not endorsed by MultiMC. The PolyMC community felt that the maintainer was not acting in the spirit of Free Software so this fork was made. +This is a **fork** of the MultiMC Launcher and not endorsed by MultiMC. +If you want to read about why this fork was created, check out [our FAQ page](https://polymc.org/wiki/overview/faq/).
# Installation @@ -81,8 +82,8 @@ To modify download information or change packaging information send a pull reque Do whatever you want, we don't care. Just follow the license. If you have any questions about this feel free to ask in an issue. -All launcher code is available under the GPL-3 license. +All launcher code is available under the GPL-3.0-only license. -[Source for the website](https://github.com/PolyMC/polymc.github.io) is hosted under the AGPL-3 License. +[Source for the website](https://github.com/PolyMC/polymc.github.io) is hosted under the AGPL-3.0-or-later License. The logo and related assets are under the CC BY-SA 4.0 license. From 234a9e48e9abfdd67fe55760e2182dbca80fe7b4 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 21 Apr 2022 22:53:44 +0200 Subject: [PATCH 299/605] chore: add FUNDING --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..247675fc --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +open_collective: polymc From c1386bcb048beac3d51ea0734c7e6acc00a31962 Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:12:20 +0200 Subject: [PATCH 300/605] added: Mnemonics for Settings/Launcher --- launcher/ui/pages/global/LauncherPage.ui | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 4cc2a113..086de17b 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -196,7 +196,7 @@ - By &last launched + &By last launched sortingModeGroup @@ -293,7 +293,7 @@ - Colors + &Colors themeComboBoxColors @@ -334,7 +334,7 @@ The menubar is more friendly for keyboard-driven interaction. - Replace toolbar with menubar + &Replace toolbar with menubar @@ -370,21 +370,21 @@ - Show console while the game is running? + Show console while the game is &running? - Automatically close console when the game quits? + &Automatically close console when the game quits? - Show console when the game crashes? + Show console when the game &crashes? @@ -394,13 +394,13 @@ - History limit + &History limit - Stop logging when log overflows + &Stop logging when log overflows @@ -441,7 +441,7 @@ - Console font + Console &font From f52b7c030ff537d6465417adc722c23da7423bc3 Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:14:24 +0200 Subject: [PATCH 301/605] added: Mnemonics for Settings/Minecraft+ --- launcher/ui/pages/global/MinecraftPage.ui | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index c18ab34b..353390bd 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -51,7 +51,7 @@ - Start Minecraft maximized? + Start Minecraft &maximized? @@ -60,7 +60,7 @@ - Window hei&ght: + Window &height: windowHeightSpinBox @@ -70,7 +70,7 @@ - W&indow width: + Window &width: windowWidthSpinBox @@ -120,14 +120,14 @@ - Use system installation of GLFW + Use system installation of &GLFW - Use system installation of OpenAL + Use system installation of &OpenAL @@ -143,21 +143,21 @@ - Show time spent playing instances + Show time spent &playing instances - Show time spent playing across all instances + Show time spent playing across &all instances - Record time spent playing instances + &Record time spent playing instances @@ -176,7 +176,7 @@ <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 + &Close the launcher after game window opens @@ -186,7 +186,7 @@ <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 + &Quit the launcher after game window closes From 75826aca13d5dcafb630c955f8349c3354503003 Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:16:11 +0200 Subject: [PATCH 302/605] added: Mnemonics for Settings/Java --- launcher/ui/pages/global/JavaPage.ui | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index d27b200f..bb195770 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -70,14 +70,14 @@ - Minimum memory allocation: + &Minimum memory allocation: - Maximum memory allocation: + Ma&ximum memory allocation: @@ -106,7 +106,7 @@ - PermGen: + &PermGen: @@ -150,7 +150,7 @@ - Java path: + &Java path: @@ -192,7 +192,7 @@ - JVM arguments: + J&VM arguments: @@ -205,7 +205,7 @@ - Auto-detect... + &Auto-detect...
@@ -218,7 +218,7 @@ - Test + &Test @@ -234,7 +234,7 @@ If enabled, the launcher will not check if an instance is compatible with the selected Java version. - Skip Java compatibility checks + &Skip Java compatibility checks From 5a5797d9144d377e92ffaf9776b49ed60a2e8acb Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:18:39 +0200 Subject: [PATCH 303/605] added: Mnemonics for Settings/Custom Commands --- launcher/ui/widgets/CustomCommands.ui | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index 650a9cc1..68e680a5 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -29,7 +29,7 @@ true - Cus&tom Commands + &Custom Commands true @@ -41,7 +41,7 @@ - Post-exit command: + P&ost-exit command: @@ -51,7 +51,7 @@ - Pre-launch command: + &Pre-launch command: @@ -61,7 +61,7 @@ - Wrapper command: + &Wrapper command: From 717067e9ebaaaa25d818da9942e54f0deb081781 Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:19:54 +0200 Subject: [PATCH 304/605] added: Mnemonics for Settings/Proxy --- launcher/ui/pages/global/ProxyPage.ui | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/launcher/ui/pages/global/ProxyPage.ui b/launcher/ui/pages/global/ProxyPage.ui index 347fa86c..5a2fc73d 100644 --- a/launcher/ui/pages/global/ProxyPage.ui +++ b/launcher/ui/pages/global/ProxyPage.ui @@ -81,7 +81,7 @@ - SOC&KS5 + &SOCKS5 proxyGroup @@ -91,7 +91,7 @@ - H&TTP + &HTTP proxyGroup @@ -104,7 +104,7 @@ - Address and Port + &Address and Port @@ -145,14 +145,14 @@ - Username: + &Username: - Password: + &Password: From 94a655b055cd977b405223d3e549d61a4b11658b Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:20:54 +0200 Subject: [PATCH 305/605] added: Mnemonics for Settings/External Tools --- launcher/ui/pages/global/ExternalToolsPage.ui | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/ui/pages/global/ExternalToolsPage.ui b/launcher/ui/pages/global/ExternalToolsPage.ui index e79e9388..8609d469 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.ui +++ b/launcher/ui/pages/global/ExternalToolsPage.ui @@ -36,7 +36,7 @@ - JProfiler + J&Profiler @@ -73,7 +73,7 @@ - JVisualVM + J&VisualVM @@ -110,7 +110,7 @@ - MCEdit + &MCEdit @@ -156,7 +156,7 @@ - Text Editor: + &Text Editor: From 08b1b2669a864873c52d604994bb8ff373b81dbc Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:22:50 +0200 Subject: [PATCH 306/605] added: Mnemonics for Settings/Accounts --- launcher/ui/pages/global/AccountListPage.ui | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui index d21a92e2..469955b5 100644 --- a/launcher/ui/pages/global/AccountListPage.ui +++ b/launcher/ui/pages/global/AccountListPage.ui @@ -65,17 +65,17 @@ - Add Mojang + Add &Mojang - Remove + Remo&ve - Set Default + &Set Default @@ -83,17 +83,17 @@ true - No Default + &No Default - Upload Skin + &Upload Skin - Delete Skin + &Delete Skin Delete the currently active skin and go back to the default one @@ -101,17 +101,17 @@ - Add Microsoft + &Add Microsoft - Add Offline + Add &Offline - Refresh + &Refresh Refresh the account tokens From c86ec0bd36f3ac445a234b38df19f7b1bf300fbc Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:23:36 +0200 Subject: [PATCH 307/605] added: Mnemonics for Settings/APIs --- launcher/ui/pages/global/APIPage.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 7a9088d1..acde9aef 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -36,7 +36,7 @@ - Pastebin URL + &Pastebin URL @@ -98,7 +98,7 @@ - Microsoft Authentication + &Microsoft Authentication From 71777e7a6f5cc6a641c38867a4d087efdb644606 Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:31:03 +0200 Subject: [PATCH 308/605] added and fixed some Mnemonics in MainWindow --- launcher/ui/MainWindow.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 7ac4d2d4..f34cf1ab 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -294,14 +294,14 @@ public: actionViewInstanceFolder = TranslatedAction(MainWindow); actionViewInstanceFolder->setObjectName(QStringLiteral("actionViewInstanceFolder")); actionViewInstanceFolder->setIcon(APPLICATION->getThemedIcon("viewfolder")); - actionViewInstanceFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Instance Folder")); + actionViewInstanceFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&View Instance Folder")); actionViewInstanceFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the instance folder in a file browser.")); all_actions.append(&actionViewInstanceFolder); actionViewCentralModsFolder = TranslatedAction(MainWindow); actionViewCentralModsFolder->setObjectName(QStringLiteral("actionViewCentralModsFolder")); actionViewCentralModsFolder->setIcon(APPLICATION->getThemedIcon("centralmods")); - actionViewCentralModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Central Mods Folder")); + actionViewCentralModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View &Central Mods Folder")); actionViewCentralModsFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the central mods folder in a file browser.")); all_actions.append(&actionViewCentralModsFolder); @@ -326,7 +326,7 @@ public: actionSettings->setObjectName(QStringLiteral("actionSettings")); actionSettings->setIcon(APPLICATION->getThemedIcon("settings")); actionSettings->setMenuRole(QAction::PreferencesRole); - actionSettings.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Settings...")); + actionSettings.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Setti&ngs...")); actionSettings.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change settings.")); actionSettings->setShortcut(QKeySequence::Preferences); all_actions.append(&actionSettings); @@ -542,7 +542,7 @@ public: actionOpenWiki = TranslatedAction(MainWindow); actionOpenWiki->setObjectName(QStringLiteral("actionOpenWiki")); - actionOpenWiki.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 He&lp")); + actionOpenWiki.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 &Help")); actionOpenWiki.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 wiki")); connect(actionOpenWiki, &QAction::triggered, MainWindow, &MainWindow::on_actionOpenWiki_triggered); all_actions.append(&actionOpenWiki); From 45783c1661c8d39880e69b33ab014f94250bb67e Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Thu, 7 Apr 2022 19:46:41 +0100 Subject: [PATCH 309/605] ATLauncher: Support using share codes --- buildconfig/BuildConfig.h | 2 + launcher/CMakeLists.txt | 2 + .../modplatform/atlauncher/ATLShareCode.cpp | 60 ++++++++ .../modplatform/atlauncher/ATLShareCode.h | 47 ++++++ .../atlauncher/AtlOptionalModDialog.cpp | 144 ++++++++++++++++-- .../atlauncher/AtlOptionalModDialog.h | 50 ++++-- .../atlauncher/AtlOptionalModDialog.ui | 20 +-- 7 files changed, 292 insertions(+), 33 deletions(-) create mode 100644 launcher/modplatform/atlauncher/ATLShareCode.cpp create mode 100644 launcher/modplatform/atlauncher/ATLShareCode.h diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 6304387c..c1d34708 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify @@ -139,6 +140,7 @@ public: QString LEGACY_FTB_CDN_BASE_URL = "https://dist.creeper.host/FTB2/"; QString ATL_DOWNLOAD_SERVER_URL = "https://download.nodecdn.net/containers/atl/"; + QString ATL_API_BASE_URL = "https://api.atlauncher.com/v1/"; QString TECHNIC_API_BASE_URL = "https://api.technicpack.net/"; /** diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6ed86726..e60c4d45 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -557,6 +557,8 @@ set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLPackInstallTask.h modplatform/atlauncher/ATLPackManifest.cpp modplatform/atlauncher/ATLPackManifest.h + modplatform/atlauncher/ATLShareCode.cpp + modplatform/atlauncher/ATLShareCode.h ) add_unit_test(Index diff --git a/launcher/modplatform/atlauncher/ATLShareCode.cpp b/launcher/modplatform/atlauncher/ATLShareCode.cpp new file mode 100644 index 00000000..59030c87 --- /dev/null +++ b/launcher/modplatform/atlauncher/ATLShareCode.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ATLShareCode.h" + +#include "Json.h" + +namespace ATLauncher { + +static void loadShareCodeMod(ShareCodeMod& m, QJsonObject& obj) +{ + m.selected = Json::requireBoolean(obj, "selected"); + m.name = Json::requireString(obj, "name"); +} + +static void loadShareCode(ShareCode& c, QJsonObject& obj) +{ + c.pack = Json::requireString(obj, "pack"); + c.version = Json::requireString(obj, "version"); + + auto mods = Json::requireObject(obj, "mods"); + auto optional = Json::requireArray(mods, "optional"); + for (const auto modRaw : optional) { + auto modObj = Json::requireObject(modRaw); + ShareCodeMod mod; + loadShareCodeMod(mod, modObj); + c.mods.append(mod); + } +} + +void loadShareCodeResponse(ShareCodeResponse& r, QJsonObject& obj) +{ + r.error = Json::requireBoolean(obj, "error"); + r.code = Json::requireInteger(obj, "code"); + + if (obj.contains("message") && !obj.value("message").isNull()) + r.message = Json::requireString(obj, "message"); + + if (!r.error) { + auto dataRaw = Json::requireObject(obj, "data"); + loadShareCode(r.data, dataRaw); + } +} + +} diff --git a/launcher/modplatform/atlauncher/ATLShareCode.h b/launcher/modplatform/atlauncher/ATLShareCode.h new file mode 100644 index 00000000..88c30c98 --- /dev/null +++ b/launcher/modplatform/atlauncher/ATLShareCode.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +namespace ATLauncher { + +struct ShareCodeMod { + bool selected; + QString name; +}; + +struct ShareCode { + QString pack; + QString version; + QVector mods; +}; + +struct ShareCodeResponse { + bool error; + int code; + QString message; + ShareCode data; +}; + +void loadShareCodeResponse(ShareCodeResponse& r, QJsonObject& obj); + +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index ac3869dc..39ecf179 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -1,30 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2021 Jamie Mansfield + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "AtlOptionalModDialog.h" #include "ui_AtlOptionalModDialog.h" +#include +#include +#include "BuildConfig.h" +#include "Json.h" +#include "modplatform/atlauncher/ATLShareCode.h" +#include "Application.h" + AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, QVector mods) : QAbstractListModel(parent), m_mods(mods) { - // fill mod index for (int i = 0; i < m_mods.size(); i++) { auto mod = m_mods.at(i); m_index[mod.name] = i; } + // set initial state for (int i = 0; i < m_mods.size(); i++) { auto mod = m_mods.at(i); @@ -77,7 +103,7 @@ QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const } } - return QVariant(); + return {}; } bool AtlOptionalModListModel::setData(const QModelIndex &index, const QVariant &value, int role) { @@ -104,7 +130,7 @@ QVariant AtlOptionalModListModel::headerData(int section, Qt::Orientation orient } } - return QVariant(); + return {}; } Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const { @@ -115,6 +141,69 @@ Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const { return flags; } +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::Download::makeByteArray(QUrl(url), &m_response)); + + connect(m_jobPtr.get(), &NetJob::succeeded, + this, &AtlOptionalModListModel::shareCodeSuccess); + connect(m_jobPtr.get(), &NetJob::failed, + this, &AtlOptionalModListModel::shareCodeFailure); + + m_jobPtr->start(); +} + +void AtlOptionalModListModel::shareCodeSuccess() { + m_jobPtr.reset(); + + QJsonParseError parse_error {}; + auto doc = QJsonDocument::fromJson(m_response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << m_response; + return; + } + auto obj = doc.object(); + + ATLauncher::ShareCodeResponse response; + try { + ATLauncher::loadShareCodeResponse(response, obj); + } + catch (const JSONValidationError& e) { + qDebug() << QString::fromUtf8(m_response); + qWarning() << "Error while reading response from ATLauncher: " << e.cause(); + return; + } + + if (response.error) { + // fixme: plumb in an error message + qWarning() << "ATLauncher API Response Error" << response.message; + return; + } + + // FIXME: verify pack and version, error if not matching. + + // Clear the current selection + for (const auto& mod : m_mods) { + m_selection[mod.name] = false; + } + + // Make the selections, as per the share code. + for (const auto& mod : response.data.mods) { + m_selection[mod.name] = mod.selected; + } + + emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), + AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); +} + +void AtlOptionalModListModel::shareCodeFailure(const QString& reason) { + m_jobPtr.reset(); + + // fixme: plumb in an error message +} + void AtlOptionalModListModel::selectRecommended() { for (const auto& mod : m_mods) { m_selection[mod.name] = mod.recommended; @@ -212,6 +301,8 @@ AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVectortreeView->header()->setSectionResizeMode( AtlOptionalModListModel::DescriptionColumn, QHeaderView::Stretch); + connect(ui->shareCodeButton, &QPushButton::pressed, + this, &AtlOptionalModDialog::useShareCode); connect(ui->selectRecommendedButton, &QPushButton::pressed, listModel, &AtlOptionalModListModel::selectRecommended); connect(ui->clearAllButton, &QPushButton::pressed, @@ -223,3 +314,30 @@ AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVectoruseShareCode(shareCode); +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index 9832014c..953b288e 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -1,17 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2021 Jamie Mansfield + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -20,6 +39,7 @@ #include #include "modplatform/atlauncher/ATLPackIndex.h" +#include "net/NetJob.h" namespace Ui { class AtlOptionalModDialog; @@ -49,7 +69,12 @@ public: Qt::ItemFlags flags(const QModelIndex &index) const override; + void useShareCode(const QString& code); + public slots: + void shareCodeSuccess(); + void shareCodeFailure(const QString& reason); + void selectRecommended(); void clearAll(); @@ -58,6 +83,9 @@ private: void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true); private: + NetJob::Ptr m_jobPtr; + QByteArray m_response; + QVector m_mods; QMap m_selection; QMap m_index; @@ -75,6 +103,8 @@ public: return listModel->getResult(); } + void useShareCode(); + private: Ui::AtlOptionalModDialog *ui; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui index 4c5c2ec5..d9496142 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui @@ -24,6 +24,16 @@ + + + + true + + + Use Share Code + + + @@ -31,16 +41,6 @@ - - - - false - - - Use Share Code - - - From ba9059c7c8332d469a09515b4d16589909cf9bfd Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Fri, 22 Apr 2022 20:33:42 +0100 Subject: [PATCH 310/605] ATLauncher: Replace usage of QPushButton::pressed with ::clicked --- .../pages/modplatform/atlauncher/AtlOptionalModDialog.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index 39ecf179..26aa60af 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -301,13 +301,13 @@ AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVectortreeView->header()->setSectionResizeMode( AtlOptionalModListModel::DescriptionColumn, QHeaderView::Stretch); - connect(ui->shareCodeButton, &QPushButton::pressed, + connect(ui->shareCodeButton, &QPushButton::clicked, this, &AtlOptionalModDialog::useShareCode); - connect(ui->selectRecommendedButton, &QPushButton::pressed, + connect(ui->selectRecommendedButton, &QPushButton::clicked, listModel, &AtlOptionalModListModel::selectRecommended); - connect(ui->clearAllButton, &QPushButton::pressed, + connect(ui->clearAllButton, &QPushButton::clicked, listModel, &AtlOptionalModListModel::clearAll); - connect(ui->installButton, &QPushButton::pressed, + connect(ui->installButton, &QPushButton::clicked, this, &QDialog::close); } From 8bcbe07c87ee4b776d9ba743bb598f22ee80dda0 Mon Sep 17 00:00:00 2001 From: TheCodex6824 Date: Thu, 21 Apr 2022 16:01:55 -0400 Subject: [PATCH 311/605] Fix Mojang auth failing due to Mojang rejecting requests to the profile endpoint --- launcher/CMakeLists.txt | 2 + launcher/minecraft/auth/Parsers.cpp | 170 ++++++++++++++++++ launcher/minecraft/auth/Parsers.h | 1 + launcher/minecraft/auth/Yggdrasil.cpp | 22 +++ launcher/minecraft/auth/flows/Mojang.cpp | 6 +- .../auth/steps/MinecraftProfileStepMojang.cpp | 94 ++++++++++ .../auth/steps/MinecraftProfileStepMojang.h | 22 +++ 7 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp create mode 100644 launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6ed86726..075c183a 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -235,6 +235,8 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/MigrationEligibilityStep.h minecraft/auth/steps/MinecraftProfileStep.cpp minecraft/auth/steps/MinecraftProfileStep.h + minecraft/auth/steps/MinecraftProfileStepMojang.cpp + minecraft/auth/steps/MinecraftProfileStepMojang.h minecraft/auth/steps/MSAStep.cpp minecraft/auth/steps/MSAStep.h minecraft/auth/steps/XboxAuthorizationStep.cpp diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index 2dd36562..82f23559 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -212,6 +212,176 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { return true; } +namespace { + // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee) + // they are needed because the session server doesn't return skin urls for default skins + static const QString SKIN_URL_STEVE = "http://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; + static const QString SKIN_URL_ALEX = "http://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; + + bool isDefaultModelSteve(QString uuid) { + // need to calculate *Java* hashCode of UUID + // if number is even, skin/model is steve, otherwise it is alex + + // just in case dashes are in the id + uuid.remove('-'); + + if (uuid.size() != 32) { + return true; + } + + // qulonglong is guaranteed to be 64 bits + // we need to use unsigned numbers to guarantee truncation below + qulonglong most = uuid.left(16).toULongLong(nullptr, 16); + qulonglong least = uuid.right(16).toULongLong(nullptr, 16); + qulonglong xored = most ^ least; + return ((static_cast(xored >> 32)) ^ static_cast(xored)) % 2 == 0; + } +} + +/** +Uses session server for skin/cape lookup instead of profile, +because locked Mojang accounts cannot access profile endpoint +(https://api.minecraftservices.com/minecraft/profile/) + +ref: https://wiki.vg/Mojang_API#UUID_to_Profile_and_Skin.2FCape + +{ + "id": "", + "name": "", + "properties": [ + { + "name": "textures", + "value": "" + } + ] +} + +decoded base64 "value": +{ + "timestamp": , + "profileId": "", + "profileName": "", + "textures": { + "SKIN": { + "url": "" + }, + "CAPE": { + "url": "" + } + } +} +*/ + +bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { + qDebug() << "Parsing Minecraft profile..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + if(!getString(obj.value("id"), output.id)) { + qWarning() << "Minecraft profile id is not a string"; + return false; + } + + if(!getString(obj.value("name"), output.name)) { + qWarning() << "Minecraft profile name is not a string"; + return false; + } + + auto propsArray = obj.value("properties").toArray(); + QByteArray texturePayload; + for( auto p : propsArray) { + auto pObj = p.toObject(); + auto name = pObj.value("name"); + if (!name.isString() || name.toString() != "textures") { + continue; + } + + auto value = pObj.value("value"); + if (value.isString()) { + texturePayload = QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors); + } + + if (!texturePayload.isEmpty()) { + break; + } + } + + if (texturePayload.isNull()) { + qWarning() << "No texture payload data"; + return false; + } + + doc = QJsonDocument::fromJson(texturePayload, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response as JSON: " << jsonError.errorString(); + return false; + } + + obj = doc.object(); + auto textures = obj.value("textures"); + if (!textures.isObject()) { + qWarning() << "No textures array in response"; + return false; + } + + Skin skinOut; + // fill in default skin info ourselves, as this endpoint doesn't provide it + bool steve = isDefaultModelSteve(output.id); + skinOut.variant = steve ? "classic" : "slim"; + skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX; + // sadly we can't figure this out, but I don't think it really matters... + skinOut.id = "00000000-0000-0000-0000-000000000000"; + Cape capeOut; + auto tObj = textures.toObject(); + for (auto idx = tObj.constBegin(); idx != tObj.constEnd(); ++idx) { + if (idx->isObject()) { + if (idx.key() == "SKIN") { + auto skin = idx->toObject(); + if (!getString(skin.value("url"), skinOut.url)) { + qWarning() << "Skin url is not a string"; + return false; + } + + auto maybeMeta = skin.find("metadata"); + if (maybeMeta != skin.end() && maybeMeta->isObject()) { + auto meta = maybeMeta->toObject(); + // might not be present + getString(meta.value("model"), skinOut.variant); + } + } + else if (idx.key() == "CAPE") { + auto cape = idx->toObject(); + if (!getString(cape.value("url"), capeOut.url)) { + qWarning() << "Cape url is not a string"; + return false; + } + + // 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 :( + capeOut.alias = "cape"; + } + } + } + + output.skin = skinOut; + if (capeOut.alias == "cape") { + output.capes = QMap({{capeOut.alias, capeOut}}); + output.currentCape = capeOut.alias; + } + + output.validity = Katabasis::Validity::Certain; + return true; +} + bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) { qDebug() << "Parsing Minecraft entitlements..."; #ifndef NDEBUG diff --git a/launcher/minecraft/auth/Parsers.h b/launcher/minecraft/auth/Parsers.h index dac7f69b..2666d890 100644 --- a/launcher/minecraft/auth/Parsers.h +++ b/launcher/minecraft/auth/Parsers.h @@ -14,6 +14,7 @@ namespace Parsers bool parseMojangResponse(QByteArray &data, Katabasis::Token &output); bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output); + bool parseMinecraftProfileMojang(QByteArray &data, MinecraftProfile &output); bool parseMinecraftEntitlements(QByteArray &data, MinecraftEntitlement &output); bool parseRolloutResponse(QByteArray &data, bool& result); } diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp index 7ac842a6..29978411 100644 --- a/launcher/minecraft/auth/Yggdrasil.cpp +++ b/launcher/minecraft/auth/Yggdrasil.cpp @@ -209,6 +209,28 @@ void Yggdrasil::processResponse(QJsonObject responseData) { m_data->yggdrasilToken.validity = Katabasis::Validity::Certain; m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); + // Get UUID here since we need it for later + auto profile = responseData.value("selectedProfile"); + if (!profile.isObject()) { + changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a selected profile.")); + return; + } + + auto profileObj = profile.toObject(); + for (auto i = profileObj.constBegin(); i != profileObj.constEnd(); ++i) { + if (i.key() == "name" && i.value().isString()) { + m_data->minecraftProfile.name = i->toString(); + } + else if (i.key() == "id" && i.value().isString()) { + m_data->minecraftProfile.id = i->toString(); + } + } + + if (m_data->minecraftProfile.id.isEmpty()) { + changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a UUID in selected profile.")); + return; + } + // We've made it through the minefield of possible errors. Return true to indicate that // we've succeeded. qDebug() << "Finished reading authentication response."; diff --git a/launcher/minecraft/auth/flows/Mojang.cpp b/launcher/minecraft/auth/flows/Mojang.cpp index 4661dbe2..b86b0936 100644 --- a/launcher/minecraft/auth/flows/Mojang.cpp +++ b/launcher/minecraft/auth/flows/Mojang.cpp @@ -1,7 +1,7 @@ #include "Mojang.h" #include "minecraft/auth/steps/YggdrasilStep.h" -#include "minecraft/auth/steps/MinecraftProfileStep.h" +#include "minecraft/auth/steps/MinecraftProfileStepMojang.h" #include "minecraft/auth/steps/MigrationEligibilityStep.h" #include "minecraft/auth/steps/GetSkinStep.h" @@ -10,7 +10,7 @@ MojangRefresh::MojangRefresh( QObject *parent ) : AuthFlow(data, parent) { m_steps.append(new YggdrasilStep(m_data, QString())); - m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new MinecraftProfileStepMojang(m_data)); m_steps.append(new MigrationEligibilityStep(m_data)); m_steps.append(new GetSkinStep(m_data)); } @@ -21,7 +21,7 @@ MojangLogin::MojangLogin( QObject *parent ): AuthFlow(data, parent), m_password(password) { m_steps.append(new YggdrasilStep(m_data, m_password)); - m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new MinecraftProfileStepMojang(m_data)); m_steps.append(new MigrationEligibilityStep(m_data)); m_steps.append(new GetSkinStep(m_data)); } diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp new file mode 100644 index 00000000..d3035272 --- /dev/null +++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp @@ -0,0 +1,94 @@ +#include "MinecraftProfileStepMojang.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) { + +} + +MinecraftProfileStepMojang::~MinecraftProfileStepMojang() noexcept = default; + +QString MinecraftProfileStepMojang::describe() { + return tr("Fetching the Minecraft profile."); +} + + +void MinecraftProfileStepMojang::perform() { + if (m_data->minecraftProfile.id.isEmpty()) { + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile.")); + return; + } + + // use session server instead of profile due to profile endpoint being locked for locked Mojang accounts + QUrl url = QUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + m_data->minecraftProfile.id); + QNetworkRequest req = QNetworkRequest(url); + AuthRequest *request = new AuthRequest(this); + connect(request, &AuthRequest::finished, this, &MinecraftProfileStepMojang::onRequestDone); + request->get(req); +} + +void MinecraftProfileStepMojang::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void MinecraftProfileStepMojang::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + if (error == QNetworkReply::ContentNotFoundError) { + // NOTE: Succeed even if we do not have a profile. This is a valid account state. + if(m_data->type == AccountType::Mojang) { + m_data->minecraftEntitlement.canPlayMinecraft = false; + m_data->minecraftEntitlement.ownsMinecraft = false; + } + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_SUCCEEDED, + tr("Account has no Minecraft profile.") + ); + return; + } + if (error != QNetworkReply::NoError) { + qWarning() << "Error getting profile:"; + qWarning() << " HTTP Status: " << requestor->httpStatus_; + qWarning() << " Internal error no.: " << error; + qWarning() << " Error string: " << requestor->errorString_; + + qWarning() << " Response:"; + qWarning() << QString::fromUtf8(data); + + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile acquisition failed.") + ); + return; + } + if(!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) { + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile response could not be parsed") + ); + return; + } + + if(m_data->type == AccountType::Mojang) { + auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain; + m_data->minecraftEntitlement.canPlayMinecraft = validProfile; + m_data->minecraftEntitlement.ownsMinecraft = validProfile; + } + emit finished( + AccountTaskState::STATE_WORKING, + tr("Minecraft Java profile acquisition succeeded.") + ); +} diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h new file mode 100644 index 00000000..e06b30ab --- /dev/null +++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class MinecraftProfileStepMojang : public AuthStep { + Q_OBJECT + +public: + explicit MinecraftProfileStepMojang(AccountData *data); + virtual ~MinecraftProfileStepMojang() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; From e56f0db11bc096909bc17eac1a5cdf5de06817dc Mon Sep 17 00:00:00 2001 From: TheCodex6824 Date: Sat, 23 Apr 2022 10:32:52 -0400 Subject: [PATCH 312/605] Remove base64 decode option that was added in Qt 5.15 --- launcher/minecraft/auth/Parsers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index 82f23559..60458b9b 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -307,7 +307,7 @@ bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { auto value = pObj.value("value"); if (value.isString()) { - texturePayload = QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors); + texturePayload = QByteArray::fromBase64(value.toString().toUtf8()); } if (!texturePayload.isEmpty()) { From a0bafa49520195512c388ebe8d5e5b307d0a10be Mon Sep 17 00:00:00 2001 From: TheCodex6824 Date: Sat, 23 Apr 2022 11:11:55 -0400 Subject: [PATCH 313/605] Re-add base64 decode option for Qt versions that support it --- launcher/minecraft/auth/Parsers.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index 60458b9b..ea882b56 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -213,8 +213,8 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { } namespace { - // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee) - // they are needed because the session server doesn't return skin urls for default skins + // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee) + // they are needed because the session server doesn't return skin urls for default skins static const QString SKIN_URL_STEVE = "http://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; static const QString SKIN_URL_ALEX = "http://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; @@ -307,7 +307,11 @@ bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { auto value = pObj.value("value"); if (value.isString()) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + texturePayload = QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors); +#else texturePayload = QByteArray::fromBase64(value.toString().toUtf8()); +#endif } if (!texturePayload.isEmpty()) { From c968c1be7892fbc1ba0571d30a03b20e3f8a5abc Mon Sep 17 00:00:00 2001 From: icelimetea Date: Sun, 24 Apr 2022 14:45:01 +0100 Subject: [PATCH 314/605] Refactor some parts of NewLaunch --- .../launcher/org/multimc/EntryPoint.java | 155 ++++++++---------- libraries/launcher/org/multimc/Launcher.java | 2 +- .../launcher/org/multimc/ParamBucket.java | 51 +++--- 3 files changed, 95 insertions(+), 113 deletions(-) diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index 0f904f5f..c923bbde 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -16,22 +16,24 @@ package org.multimc;/* import org.multimc.onesix.OneSixLauncher; -import java.io.*; -import java.nio.charset.Charset; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; public class EntryPoint { - private enum Action - { - Proceed, - Launch, - Abort - } + + private final ParamBucket params = new ParamBucket(); + + private org.multimc.Launcher launcher; public static void main(String[] args) { EntryPoint listener = new EntryPoint(); + int retCode = listener.listen(); + if (retCode != 0) { System.out.println("Exiting with " + retCode); @@ -41,111 +43,92 @@ public class EntryPoint private Action parseLine(String inData) throws ParseException { - String[] pair = inData.split(" ", 2); + String[] pair = inData.split("\\s+", 2); - if(pair.length == 1) - { - String command = pair[0]; - if (pair[0].equals("launch")) + if (pair.length == 0) + throw new ParseException("Unexpected empty string!"); + + switch (pair[0]) { + case "launch": { return Action.Launch; + } - else if (pair[0].equals("abort")) + case "abort": { return Action.Abort; + } - else throw new ParseException("Error while parsing:" + pair[0]); - } + case "launcher": { + if (pair.length != 2) + throw new ParseException("Expected 2 tokens, got 1!"); - if(pair.length != 2) - throw new ParseException("Pair length is not 2."); + if (pair[1].equals("onesix")) { + launcher = new OneSixLauncher(); - String command = pair[0]; - String param = pair[1]; + Utils.log("Using onesix launcher."); + + return Action.Proceed; + } else { + throw new ParseException("Invalid launcher type: " + pair[1]); + } + } + + default: { + if (pair.length != 2) + throw new ParseException("Error while parsing:" + pair[0]); + + params.add(pair[0], pair[1]); - if(command.equals("launcher")) - { - if(param.equals("onesix")) - { - m_launcher = new OneSixLauncher(); - Utils.log("Using onesix launcher."); - Utils.log(); return Action.Proceed; } - else - throw new ParseException("Invalid launcher type: " + param); } - - m_params.add(command, param); - //System.out.println(command + " : " + param); - return Action.Proceed; } public int listen() { - BufferedReader buffer; - try - { - buffer = new BufferedReader(new InputStreamReader(System.in, "UTF-8")); - } catch (UnsupportedEncodingException e) - { - System.err.println("For some reason, your java does not support UTF-8. Consider living in the current century."); + Action action = Action.Proceed; + + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + System.in, + StandardCharsets.UTF_8 + ))) { + String line; + + while (action == Action.Proceed) { + if ((line = reader.readLine()) != null) { + action = parseLine(line); + } else { + action = Action.Abort; + } + } + } catch (IOException | ParseException e) { + Utils.log("Launcher ABORT due to exception:"); + e.printStackTrace(); + return 1; } - boolean isListening = true; - boolean isAborted = false; + // Main loop - while (isListening) - { - String inData; - try - { - // Read from the pipe one line at a time - inData = buffer.readLine(); - if (inData != null) - { - Action a = parseLine(inData); - if(a == Action.Abort) - { - isListening = false; - isAborted = true; - } - if(a == Action.Launch) - { - isListening = false; - } - } - else - { - isListening = false; - isAborted = true; - } - } - catch (IOException e) - { - System.err.println("Launcher ABORT due to IO exception:"); - e.printStackTrace(); - return 1; - } - catch (ParseException e) - { - System.err.println("Launcher ABORT due to PARSE exception:"); - e.printStackTrace(); - return 1; - } - } - if(isAborted) + if (action == Action.Abort) { System.err.println("Launch aborted by the launcher."); return 1; } - if(m_launcher != null) + + if (launcher != null) { - return m_launcher.launch(m_params); + return launcher.launch(params); } + System.err.println("No valid launcher implementation specified."); + return 1; } - private ParamBucket m_params = new ParamBucket(); - private org.multimc.Launcher m_launcher; + private enum Action { + Proceed, + Launch, + Abort + } + } diff --git a/libraries/launcher/org/multimc/Launcher.java b/libraries/launcher/org/multimc/Launcher.java index d8cb6d1b..c5e8fbc1 100644 --- a/libraries/launcher/org/multimc/Launcher.java +++ b/libraries/launcher/org/multimc/Launcher.java @@ -18,5 +18,5 @@ package org.multimc; public interface Launcher { - abstract int launch(ParamBucket params); + int launch(ParamBucket params); } diff --git a/libraries/launcher/org/multimc/ParamBucket.java b/libraries/launcher/org/multimc/ParamBucket.java index 2fde1329..8ff03ddc 100644 --- a/libraries/launcher/org/multimc/ParamBucket.java +++ b/libraries/launcher/org/multimc/ParamBucket.java @@ -19,62 +19,62 @@ package org.multimc; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; public class ParamBucket { + + private final Map> paramsMap = new HashMap<>(); + public void add(String key, String value) { - List coll = null; - if(!m_params.containsKey(key)) - { - coll = new ArrayList(); - m_params.put(key, coll); - } - else - { - coll = m_params.get(key); - } - coll.add(value); + paramsMap.computeIfAbsent(key, k -> new ArrayList<>()) + .add(value); } public List all(String key) throws NotFoundException { - if(!m_params.containsKey(key)) + List params = paramsMap.get(key); + + if (params == null) throw new NotFoundException(); - return m_params.get(key); + + return params; } public List allSafe(String key, List def) { - if(!m_params.containsKey(key) || m_params.get(key).size() < 1) - { + List params = paramsMap.get(key); + + if (params == null || params.isEmpty()) return def; - } - return m_params.get(key); + + return params; } public List allSafe(String key) { - return allSafe(key, new ArrayList()); + return allSafe(key, new ArrayList<>()); } public String first(String key) throws NotFoundException { List list = all(key); - if(list.size() < 1) - { + + if (list.isEmpty()) throw new NotFoundException(); - } + return list.get(0); } public String firstSafe(String key, String def) { - if(!m_params.containsKey(key) || m_params.get(key).size() < 1) - { + List params = paramsMap.get(key); + + if (params == null || params.isEmpty()) return def; - } - return m_params.get(key).get(0); + + return params.get(0); } public String firstSafe(String key) @@ -82,5 +82,4 @@ public class ParamBucket return firstSafe(key, ""); } - private HashMap> m_params = new HashMap>(); } From b0a469baab54c38a80607a4567b4c0f6eb825245 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Sun, 24 Apr 2022 15:10:35 +0100 Subject: [PATCH 315/605] Use java.util.logging instead of custom logging --- .../launcher/org/multimc/EntryPoint.java | 18 +++++--- libraries/launcher/org/multimc/Utils.java | 33 ------------- .../org/multimc/onesix/OneSixLauncher.java | 46 +++++++++++-------- 3 files changed, 38 insertions(+), 59 deletions(-) diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index c923bbde..85cf3702 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -20,10 +20,14 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; public class EntryPoint { + private static final Logger LOGGER = Logger.getLogger("EntryPoint"); + private final ParamBucket params = new ParamBucket(); private org.multimc.Launcher launcher; @@ -36,7 +40,8 @@ public class EntryPoint if (retCode != 0) { - System.out.println("Exiting with " + retCode); + LOGGER.info("Exiting with " + retCode); + System.exit(retCode); } } @@ -64,7 +69,7 @@ public class EntryPoint if (pair[1].equals("onesix")) { launcher = new OneSixLauncher(); - Utils.log("Using onesix launcher."); + LOGGER.info("Using onesix launcher."); return Action.Proceed; } else { @@ -101,9 +106,7 @@ public class EntryPoint } } } catch (IOException | ParseException e) { - Utils.log("Launcher ABORT due to exception:"); - - e.printStackTrace(); + LOGGER.log(Level.SEVERE, "Launcher ABORT due to exception:", e); return 1; } @@ -111,7 +114,8 @@ public class EntryPoint // Main loop if (action == Action.Abort) { - System.err.println("Launch aborted by the launcher."); + LOGGER.info("Launch aborted by the launcher."); + return 1; } @@ -120,7 +124,7 @@ public class EntryPoint return launcher.launch(params); } - System.err.println("No valid launcher implementation specified."); + LOGGER.log(Level.SEVERE, "No valid launcher implementation specified."); return 1; } diff --git a/libraries/launcher/org/multimc/Utils.java b/libraries/launcher/org/multimc/Utils.java index 353af7d3..e48029c2 100644 --- a/libraries/launcher/org/multimc/Utils.java +++ b/libraries/launcher/org/multimc/Utils.java @@ -16,21 +16,10 @@ package org.multimc; -import java.io.*; import java.io.File; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.Arrays; -import java.util.Enumeration; import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; public class Utils { @@ -93,27 +82,5 @@ public class Utils return null; } - /** - * Log to the launcher console - * - * @param message A String containing the message - * @param level A String containing the level name. See MinecraftLauncher::getLevel() - */ - public static void log(String message, String level) - { - // Kinda dirty - String tag = "!![" + level + "]!"; - System.out.println(tag + message.replace("\n", "\n" + tag)); - } - - public static void log(String message) - { - log(message, "Launcher"); - } - - public static void log() - { - System.out.println(); - } } diff --git a/libraries/launcher/org/multimc/onesix/OneSixLauncher.java b/libraries/launcher/org/multimc/onesix/OneSixLauncher.java index ea445995..0058bd43 100644 --- a/libraries/launcher/org/multimc/onesix/OneSixLauncher.java +++ b/libraries/launcher/org/multimc/onesix/OneSixLauncher.java @@ -19,14 +19,18 @@ import org.multimc.*; import java.applet.Applet; import java.io.File; -import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; public class OneSixLauncher implements Launcher { + + private static final Logger LOGGER = Logger.getLogger("OneSixLauncher"); + // parameters, separated from ParamBucket private List libraries; private List mcparams; @@ -104,7 +108,7 @@ public class OneSixLauncher implements Launcher if (f == null) { - System.err.println("Could not find Minecraft path field."); + LOGGER.warning("Could not find Minecraft path field."); } else { @@ -113,8 +117,12 @@ public class OneSixLauncher implements Launcher } } catch (Exception e) { - System.err.println("Could not set base folder. Failed to find/access Minecraft main class:"); - e.printStackTrace(System.err); + LOGGER.log( + Level.SEVERE, + "Could not set base folder. Failed to find/access Minecraft main class:", + e + ); + return -1; } @@ -122,7 +130,7 @@ public class OneSixLauncher implements Launcher if(!traits.contains("noapplet")) { - Utils.log("Launching with applet wrapper..."); + LOGGER.info("Launching with applet wrapper..."); try { Class MCAppletClass = cl.loadClass(appletClass); @@ -132,10 +140,9 @@ public class OneSixLauncher implements Launcher return 0; } catch (Exception e) { - Utils.log("Applet wrapper failed:", "Error"); - e.printStackTrace(System.err); - Utils.log(); - Utils.log("Falling back to using main class."); + LOGGER.log(Level.SEVERE, "Applet wrapper failed:", e); + + LOGGER.warning("Falling back to using main class."); } } @@ -147,8 +154,8 @@ public class OneSixLauncher implements Launcher return 0; } catch (Exception e) { - Utils.log("Failed to invoke the Minecraft main class:", "Fatal"); - e.printStackTrace(System.err); + LOGGER.log(Level.SEVERE, "Failed to invoke the Minecraft main class:", e); + return -1; } } @@ -185,8 +192,8 @@ public class OneSixLauncher implements Launcher mc = cl.loadClass(mainClass); } catch (ClassNotFoundException e) { - System.err.println("Failed to find Minecraft main class:"); - e.printStackTrace(System.err); + LOGGER.log(Level.SEVERE, "Failed to find Minecraft main class:", e); + return -1; } @@ -197,8 +204,8 @@ public class OneSixLauncher implements Launcher meth = mc.getMethod("main", String[].class); } catch (NoSuchMethodException e) { - System.err.println("Failed to acquire the main method:"); - e.printStackTrace(System.err); + LOGGER.log(Level.SEVERE, "Failed to acquire the main method:", e); + return -1; } @@ -210,8 +217,8 @@ public class OneSixLauncher implements Launcher meth.invoke(null, (Object) paramsArray); } catch (Exception e) { - System.err.println("Failed to start Minecraft:"); - e.printStackTrace(System.err); + LOGGER.log(Level.SEVERE, "Failed to start Minecraft:", e); + return -1; } return 0; @@ -226,8 +233,8 @@ public class OneSixLauncher implements Launcher processParams(params); } catch (NotFoundException e) { - System.err.println("Not enough arguments."); - e.printStackTrace(System.err); + LOGGER.log(Level.SEVERE, "Not enough arguments!"); + return -1; } @@ -245,4 +252,5 @@ public class OneSixLauncher implements Launcher return launchWithMainClass(); } } + } From 884f7723624b68ffb23b0c30c8c3725a7e126b4a Mon Sep 17 00:00:00 2001 From: icelimetea Date: Mon, 25 Apr 2022 11:22:56 +0100 Subject: [PATCH 316/605] Clarify exception messages --- .../launcher/org/multimc/EntryPoint.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index 85cf3702..b626d095 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -48,12 +48,12 @@ public class EntryPoint private Action parseLine(String inData) throws ParseException { - String[] pair = inData.split("\\s+", 2); + String[] tokens = inData.split("\\s+", 2); - if (pair.length == 0) + if (tokens.length == 0) throw new ParseException("Unexpected empty string!"); - switch (pair[0]) { + switch (tokens[0]) { case "launch": { return Action.Launch; } @@ -63,25 +63,25 @@ public class EntryPoint } case "launcher": { - if (pair.length != 2) - throw new ParseException("Expected 2 tokens, got 1!"); + if (tokens.length != 2) + throw new ParseException("Expected 2 tokens, got " + tokens.length); - if (pair[1].equals("onesix")) { + if (tokens[1].equals("onesix")) { launcher = new OneSixLauncher(); LOGGER.info("Using onesix launcher."); return Action.Proceed; } else { - throw new ParseException("Invalid launcher type: " + pair[1]); + throw new ParseException("Invalid launcher type: " + tokens[1]); } } default: { - if (pair.length != 2) - throw new ParseException("Error while parsing:" + pair[0]); + if (tokens.length != 2) + throw new ParseException("Error while parsing:" + inData); - params.add(pair[0], pair[1]); + params.add(tokens[0], tokens[1]); return Action.Proceed; } From 1ff459d995f685aa5a83fe2c1c4b8f0f3b56ed03 Mon Sep 17 00:00:00 2001 From: TheCodex6824 Date: Mon, 25 Apr 2022 14:08:27 -0400 Subject: [PATCH 317/605] Use suggested error handling --- launcher/minecraft/auth/Parsers.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index ea882b56..47473899 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -1,4 +1,5 @@ #include "Parsers.h" +#include "Json.h" #include #include @@ -285,7 +286,7 @@ bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { return false; } - auto obj = doc.object(); + auto obj = Json::requireObject(doc, "mojang minecraft profile"); if(!getString(obj.value("id"), output.id)) { qWarning() << "Minecraft profile id is not a string"; return false; @@ -330,7 +331,7 @@ bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { return false; } - obj = doc.object(); + obj = Json::requireObject(doc, "session texture payload"); auto textures = obj.value("textures"); if (!textures.isObject()) { qWarning() << "No textures array in response"; From ac405aa564821723505b4e46865d0a9ad1e32b99 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 25 Apr 2022 19:57:47 -0400 Subject: [PATCH 318/605] Remove old macOS data migration code --- launcher/Application.cpp | 63 ----------------------- launcher/ui/pages/global/LauncherPage.cpp | 14 ----- launcher/ui/pages/global/LauncherPage.h | 1 - launcher/ui/pages/global/LauncherPage.ui | 7 --- 4 files changed, 85 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 8bd434f0..2d0bba22 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -409,69 +409,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) return; } -#if defined(Q_OS_MAC) - // move user data to new location if on macOS and it still exists in Contents/MacOS - QDir fi(applicationDirPath()); - QString originalData = fi.absolutePath(); - // if the config file exists in Contents/MacOS, then user data is still there and needs to moved - if (QFileInfo::exists(FS::PathCombine(originalData, BuildConfig.LAUNCHER_CONFIGFILE))) - { - if (!QFileInfo::exists(FS::PathCombine(originalData, "dontmovemacdata"))) - { - QMessageBox::StandardButton askMoveDialogue; - askMoveDialogue = QMessageBox::question( - nullptr, - BuildConfig.LAUNCHER_DISPLAYNAME, - "Would you like to move application data to a new data location? It will improve the launcher's performance, but if you switch to older versions it will look like instances have disappeared. If you select no, you can migrate later in settings. You should select yes unless you're commonly switching between different versions (eg. develop and stable).", - QMessageBox::Yes | QMessageBox::No, - QMessageBox::Yes - ); - if (askMoveDialogue == QMessageBox::Yes) - { - qDebug() << "On macOS and found config file in old location, moving user data..."; - QDir dir; - QStringList dataFiles { - "*.log", // Launcher log files: ${Launcher_Name}-@.log - "accounts.json", - "accounts", - "assets", - "cache", - "icons", - "instances", - "libraries", - "meta", - "metacache", - "mods", - BuildConfig.LAUNCHER_CONFIGFILE, - "themes", - "translations" - }; - QDirIterator files(originalData, dataFiles); - while (files.hasNext()) { - QString filePath(files.next()); - QString fileName(files.fileName()); - if (!dir.rename(filePath, FS::PathCombine(dataPath, fileName))) - { - qWarning() << "Failed to move " << fileName; - } - } - } - else - { - dataPath = originalData; - QDir::setCurrent(dataPath); - QFile file(originalData + "/dontmovemacdata"); - file.open(QIODevice::WriteOnly); - } - } - else - { - dataPath = originalData; - QDir::setCurrent(dataPath); - } - } -#endif - /* * Establish the mechanism for communication with an already running PolyMC that uses the same data path. * If there is one, tell it what the user actually wanted to do and exit. diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 097a2bfa..af2e2cd1 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -97,13 +97,6 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch } connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview())); connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview())); - - //move mac data button - QFile file(QDir::current().absolutePath() + "/dontmovemacdata"); - if (!file.exists()) - { - ui->migrateDataFolderMacBtn->setVisible(false); - } } LauncherPage::~LauncherPage() @@ -190,13 +183,6 @@ void LauncherPage::on_modsDirBrowseBtn_clicked() ui->modsDirTextBox->setText(cooked_dir); } } -void LauncherPage::on_migrateDataFolderMacBtn_clicked() -{ - QFile file(QDir::current().absolutePath() + "/dontmovemacdata"); - file.remove(); - QProcess::startDetached(qApp->arguments()[0]); - qApp->quit(); -} void LauncherPage::refreshUpdateChannelList() { diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index 63cfe9c3..bbf5d2fe 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -88,7 +88,6 @@ slots: void on_instDirBrowseBtn_clicked(); void on_modsDirBrowseBtn_clicked(); void on_iconsDirBrowseBtn_clicked(); - void on_migrateDataFolderMacBtn_clicked(); /*! * Updates the list of update channels in the combo box. diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 086de17b..ae7eb73f 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -157,13 +157,6 @@ - - - - Move the data to new location (will restart the launcher) - - - From 0507b56beda250d86aaf9f772c122bceae04f748 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Wed, 27 Apr 2022 20:29:40 +0800 Subject: [PATCH 319/605] feat: add PolyMC icon as instance icon --- launcher/resources/multimc/multimc.qrc | 1 + .../multimc/scalable/instances/polymc.svg | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 launcher/resources/multimc/scalable/instances/polymc.svg diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index d31885b9..0fe673ff 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -313,5 +313,6 @@ scalable/instances/fox.svg scalable/instances/bee.svg + scalable/instances/polymc.svg diff --git a/launcher/resources/multimc/scalable/instances/polymc.svg b/launcher/resources/multimc/scalable/instances/polymc.svg new file mode 100644 index 00000000..c192d503 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/polymc.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + From efe4e7df06d97e0d08987447f11d187423db1b8d Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Tue, 26 Apr 2022 18:04:05 +0200 Subject: [PATCH 320/605] fix some appimage issues building with qt 5.15.2 some users are having weird scaling issues since we're using qt 5.12.8 for the appimage --- .github/workflows/build.yml | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6d1189b..57c04e21 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,9 @@ jobs: - os: ubuntu-20.04 + - os: ubuntu-20.04 + appimage: true + - os: windows-2022 name: "Windows-i686" msystem: mingw32 @@ -66,30 +69,25 @@ jobs: ver_short=`git rev-parse --short HEAD` echo "VERSION=$ver_short" >> $GITHUB_ENV - - name: Install OpenJDK - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' - - name: Install Qt (macOS) if: runner.os == 'macOS' run: | brew update - brew install qt@5 + brew install qt@5 ninja + + - name: Update Qt (AppImage) + if: runner.os == 'Linux' && matrix.appimage == true + run: | + sudo add-apt-repository ppa:savoury1/qt-5-15 - name: Install Qt (Linux) if: runner.os == 'Linux' run: | sudo apt-get -y update - sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 - - - name: Install Ninja - if: runner.os != 'Windows' - uses: urkle/action-get-ninja@v1 + sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 ninja-build - name: Prepare AppImage (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage == true run: | wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" wget "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage" @@ -167,7 +165,7 @@ jobs: cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable - name: Package (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage != true run: | cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_DIR }} @@ -175,7 +173,7 @@ jobs: tar --owner root --group root -czf ../PolyMC.tar.gz * - name: Package (Linux, portable) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage != true run: | cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable @@ -184,7 +182,7 @@ jobs: tar -czf ../PolyMC-portable.tar.gz * - name: Package AppImage (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage == true shell: bash run: | cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr @@ -234,21 +232,21 @@ jobs: path: ${{ env.INSTALL_PORTABLE_DIR }}/** - name: Upload binary tarball (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage != true uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC.tar.gz - name: Upload binary tarball (Linux, portable) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage != true uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC-portable.tar.gz - name: Upload AppImage (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage == true uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage From b931dc0f9373a0e6887911e3d6f8fb69afbed790 Mon Sep 17 00:00:00 2001 From: txtsd Date: Fri, 29 Apr 2022 01:30:47 +0530 Subject: [PATCH 321/605] fix(mnemonics): Add missing buddies to labels Co-authored-by: Sefa Eyeoglu --- launcher/ui/pages/global/ExternalToolsPage.ui | 3 +++ launcher/ui/pages/global/JavaPage.ui | 15 +++++++++++++++ launcher/ui/pages/global/ProxyPage.ui | 6 ++++++ launcher/ui/widgets/CustomCommands.ui | 9 +++++++++ 4 files changed, 33 insertions(+) diff --git a/launcher/ui/pages/global/ExternalToolsPage.ui b/launcher/ui/pages/global/ExternalToolsPage.ui index 8609d469..3643094d 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.ui +++ b/launcher/ui/pages/global/ExternalToolsPage.ui @@ -158,6 +158,9 @@ &Text Editor: + + jsonEditorTextBox + diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index bb195770..083435d8 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -72,6 +72,9 @@ &Minimum memory allocation: + + minMemSpinBox + @@ -79,6 +82,9 @@ Ma&ximum memory allocation: + + maxMemSpinBox + @@ -108,6 +114,9 @@ &PermGen: + + permGenSpinBox + @@ -152,6 +161,9 @@ &Java path: + + javaPathTextBox + @@ -194,6 +206,9 @@ J&VM arguments: + + jvmArgsTextBox + diff --git a/launcher/ui/pages/global/ProxyPage.ui b/launcher/ui/pages/global/ProxyPage.ui index 5a2fc73d..91ba46b3 100644 --- a/launcher/ui/pages/global/ProxyPage.ui +++ b/launcher/ui/pages/global/ProxyPage.ui @@ -147,6 +147,9 @@ &Username: + + proxyUserEdit + @@ -154,6 +157,9 @@ &Password: + + proxyPassEdit + diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index 68e680a5..4a39ff7f 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -43,6 +43,9 @@ P&ost-exit command: + + postExitCmdTextBox + @@ -53,6 +56,9 @@ &Pre-launch command: + + preLaunchCmdTextBox + @@ -63,6 +69,9 @@ &Wrapper command: + + wrapperCmdTextBox + From ece5ca52b211baf4505d113b7ad8f2c939e95c51 Mon Sep 17 00:00:00 2001 From: txtsd Date: Fri, 29 Apr 2022 05:04:26 +0530 Subject: [PATCH 322/605] feat(workflow): Use ccache --- .github/workflows/build.yml | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 57c04e21..042ef27c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,6 +62,32 @@ jobs: cmake:p ninja:p qt5:p + ccache:p + + - name: Setup ccache + if: runner.os != 'Windows' + uses: hendrikmuhs/ccache-action@v1.2.1 + with: + key: ${{ matrix.os }}-${{ matrix.appimage }}-${{ inputs.build_type }} + + - name: Setup ccache (Windows) + if: runner.os == 'Windows' + shell: msys2 {0} + run: | + ccache --set-config=cache_dir='${{ github.workspace }}\.ccache' + ccache --set-config=max_size='500M' + ccache --set-config=compression=true + ccache -p # Show config + ccache -z # Zero stats + + - name: Retrieve ccache cache (Windows) + if: runner.os == 'Windows' + uses: actions/cache@v3.0.2 + with: + path: '${{ github.workspace }}\.ccache' + key: ${{ matrix.os }}-${{ matrix.msystem }}-${{ inputs.build_type }} + restore-keys: | + ${{ matrix.os }}-${{ matrix.msystem }}-${{ inputs.build_type }} - name: Set short version shell: bash @@ -102,18 +128,18 @@ jobs: - name: Configure CMake (macOS) if: runner.os == 'macOS' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DQt5_DIR=/usr/local/opt/qt@5 -DCMAKE_PREFIX_PATH=/usr/local/opt/qt@5 -DLauncher_BUILD_PLATFORM=macOS -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DQt5_DIR=/usr/local/opt/qt@5 -DCMAKE_PREFIX_PATH=/usr/local/opt/qt@5 -DLauncher_BUILD_PLATFORM=macOS -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -G Ninja - name: Configure CMake (Windows) if: runner.os == 'Windows' shell: msys2 {0} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -G Ninja - name: Configure CMake (Linux) if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -G Ninja ## # BUILD From dac801c8ac388495bfc8a4376d01457553ef6bad Mon Sep 17 00:00:00 2001 From: dada513 Date: Sat, 30 Apr 2022 15:19:57 +0200 Subject: [PATCH 323/605] add hide java wizard toggle --- launcher/Application.cpp | 6 ++ launcher/ui/pages/global/JavaPage.cpp | 2 + launcher/ui/pages/global/JavaPage.ui | 88 +++++++++++++++------------ share | 1 + 4 files changed, 58 insertions(+), 39 deletions(-) create mode 120000 share diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 8bd434f0..c8f6b780 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -691,6 +691,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("LastHostname", ""); m_settings->registerSetting("JvmArgs", ""); m_settings->registerSetting("IgnoreJavaCompatibility", false); + m_settings->registerSetting("IgnoreJavaWizard", false); // Native library workarounds m_settings->registerSetting("UseNativeOpenAL", false); @@ -936,6 +937,10 @@ bool Application::createSetupWizard() { bool javaRequired = [&]() { + bool ignoreJavaWizard = m_settings->get("IgnoreJavaWizard").toBool(); + if(ignoreJavaWizard) { + return false; + } QString currentHostName = QHostInfo::localHostName(); QString oldHostName = settings()->get("LastHostname").toString(); if (currentHostName != oldHostName) @@ -966,6 +971,7 @@ bool Application::createSetupWizard() { m_setupWizard->addPage(new LanguageWizardPage(m_setupWizard)); } + if (javaRequired) { m_setupWizard->addPage(new JavaWizardPage(m_setupWizard)); diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index f0616db1..b5e8de6c 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -97,6 +97,7 @@ void JavaPage::applySettings() s->set("JavaPath", ui->javaPathTextBox->text()); s->set("JvmArgs", ui->jvmArgsTextBox->text()); s->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked()); + s->set("IgnoreJavaWizard", ui->skipJavaWizardCheckbox->isChecked()); JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget()); } void JavaPage::loadSettings() @@ -121,6 +122,7 @@ void JavaPage::loadSettings() ui->javaPathTextBox->setText(s->get("JavaPath").toString()); ui->jvmArgsTextBox->setText(s->get("JvmArgs").toString()); ui->skipCompatibilityCheckbox->setChecked(s->get("IgnoreJavaCompatibility").toBool()); + ui->skipJavaWizardCheckbox->setChecked(s->get("IgnoreJavaWizard").toBool()); } void JavaPage::on_javaDetectBtn_clicked() diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index bb195770..7268601f 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -154,6 +154,48 @@ + + + + + 0 + 0 + + + + J&VM arguments: + + + + + + + + 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... + + + @@ -180,35 +222,6 @@ - - - - - - - - 0 - 0 - - - - J&VM arguments: - - - - - - - - 0 - 0 - - - - &Auto-detect... - - - @@ -222,19 +235,16 @@ - - - - - 0 - 0 - - + + + + + - If enabled, the launcher will not check if an instance is compatible with the selected Java version. + If enabled, the launcher will not prompt you to choose a Java version if one isn't found. - &Skip Java compatibility checks + Skip Java Wizard diff --git a/share b/share new file mode 120000 index 00000000..bf797c5f --- /dev/null +++ b/share @@ -0,0 +1 @@ +cmake-build-debug \ No newline at end of file From 67687683731fecde57042873835bb3ab844e1bce Mon Sep 17 00:00:00 2001 From: dada513 Date: Sat, 30 Apr 2022 15:22:31 +0200 Subject: [PATCH 324/605] Remove symlink --- share | 1 - 1 file changed, 1 deletion(-) delete mode 120000 share diff --git a/share b/share deleted file mode 120000 index bf797c5f..00000000 --- a/share +++ /dev/null @@ -1 +0,0 @@ -cmake-build-debug \ No newline at end of file From 1e03ef484dafc41a568442186d14dba44c42cc26 Mon Sep 17 00:00:00 2001 From: dada513 Date: Sat, 30 Apr 2022 16:14:48 +0200 Subject: [PATCH 325/605] Update launcher/ui/pages/global/JavaPage.ui Co-authored-by: Sefa Eyeoglu --- launcher/ui/pages/global/JavaPage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index 7268601f..2e553619 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -244,7 +244,7 @@ If enabled, the launcher will not prompt you to choose a Java version if one isn't found. - Skip Java Wizard + Skip Java &Wizard From 5662d410628f0df43d3b15ae493bed85915ac799 Mon Sep 17 00:00:00 2001 From: dada513 Date: Sat, 30 Apr 2022 16:20:05 +0200 Subject: [PATCH 326/605] Update launcher/ui/pages/global/JavaPage.ui Co-authored-by: Sefa Eyeoglu --- launcher/ui/pages/global/JavaPage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index 2e553619..cfcf9094 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -244,7 +244,7 @@ If enabled, the launcher will not prompt you to choose a Java version if one isn't found. - Skip Java &Wizard + Skip Java &Wizard From 239e4adf29e1190096a82ec81a8f29ff8ebf5713 Mon Sep 17 00:00:00 2001 From: txtsd Date: Sat, 30 Apr 2022 20:42:11 +0530 Subject: [PATCH 327/605] refactor(workflow): Only use ccache on Debug builds --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 042ef27c..0590b348 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,13 +65,13 @@ jobs: ccache:p - name: Setup ccache - if: runner.os != 'Windows' + if: runner.os != 'Windows' && inputs.build_type == 'Debug' uses: hendrikmuhs/ccache-action@v1.2.1 with: - key: ${{ matrix.os }}-${{ matrix.appimage }}-${{ inputs.build_type }} + key: ${{ matrix.os }}-${{ matrix.appimage }} - name: Setup ccache (Windows) - if: runner.os == 'Windows' + if: runner.os == 'Windows' && inputs.build_type == 'Debug' shell: msys2 {0} run: | ccache --set-config=cache_dir='${{ github.workspace }}\.ccache' @@ -81,13 +81,13 @@ jobs: ccache -z # Zero stats - name: Retrieve ccache cache (Windows) - if: runner.os == 'Windows' + if: runner.os == 'Windows' && inputs.build_type == 'Debug' uses: actions/cache@v3.0.2 with: path: '${{ github.workspace }}\.ccache' - key: ${{ matrix.os }}-${{ matrix.msystem }}-${{ inputs.build_type }} + key: ${{ matrix.os }}-${{ matrix.msystem }} restore-keys: | - ${{ matrix.os }}-${{ matrix.msystem }}-${{ inputs.build_type }} + ${{ matrix.os }}-${{ matrix.msystem }} - name: Set short version shell: bash From 1a86f7269002e5fb696339f0d709e4832024d741 Mon Sep 17 00:00:00 2001 From: timoreo22 Date: Mon, 2 May 2022 11:13:46 +0200 Subject: [PATCH 328/605] Fix nightly.link pr comment --- .github/workflows/pr-comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml index 7e8e4d99..f0f5b8cc 100644 --- a/.github/workflows/pr-comment.yml +++ b/.github/workflows/pr-comment.yml @@ -1,7 +1,7 @@ name: Comment on pull request on: workflow_run: - workflows: ['Test workflow with upload'] + workflows: ['Build Application'] types: [completed] jobs: pr_comment: From 0b38d878a104029892590ff7a60226ae4077aeb4 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 2 May 2022 16:27:15 +0200 Subject: [PATCH 329/605] fix: remove in-tree CMake modules where possible --- cmake/BundleUtilities.cmake | 786 --------------------------- cmake/GetPrerequisites.cmake | 902 ------------------------------- cmake/UseJava.cmake | 881 ------------------------------ cmake/UseJavaClassFilelist.cmake | 52 -- cmake/UseJavaSymlinks.cmake | 32 -- 5 files changed, 2653 deletions(-) delete mode 100644 cmake/BundleUtilities.cmake delete mode 100644 cmake/GetPrerequisites.cmake delete mode 100644 cmake/UseJava.cmake delete mode 100644 cmake/UseJavaClassFilelist.cmake delete mode 100644 cmake/UseJavaSymlinks.cmake diff --git a/cmake/BundleUtilities.cmake b/cmake/BundleUtilities.cmake deleted file mode 100644 index e3f50b94..00000000 --- a/cmake/BundleUtilities.cmake +++ /dev/null @@ -1,786 +0,0 @@ -# - Functions to help assemble a standalone bundle application. -# A collection of CMake utility functions useful for dealing with .app -# bundles on the Mac and bundle-like directories on any OS. -# -# The following functions are provided by this module: -# fixup_bundle -# copy_and_fixup_bundle -# verify_app -# get_bundle_main_executable -# get_dotapp_dir -# get_bundle_and_executable -# get_bundle_all_executables -# get_item_key -# clear_bundle_keys -# set_bundle_key_values -# get_bundle_keys -# copy_resolved_item_into_bundle -# copy_resolved_framework_into_bundle -# fixup_bundle_item -# verify_bundle_prerequisites -# verify_bundle_symlinks -# Requires CMake 2.6 or greater because it uses function, break and -# PARENT_SCOPE. Also depends on GetPrerequisites.cmake. -# -# FIXUP_BUNDLE( ) -# Fix up a bundle in-place and make it standalone, such that it can be -# drag-n-drop copied to another machine and run on that machine as long as all -# of the system libraries are compatible. -# -# If you pass plugins to fixup_bundle as the libs parameter, you should install -# them or copy them into the bundle before calling fixup_bundle. The "libs" -# parameter is a list of libraries that must be fixed up, but that cannot be -# determined by otool output analysis. (i.e., plugins) -# -# Gather all the keys for all the executables and libraries in a bundle, and -# then, for each key, copy each prerequisite into the bundle. Then fix each one -# up according to its own list of prerequisites. -# -# Then clear all the keys and call verify_app on the final bundle to ensure -# that it is truly standalone. -# -# COPY_AND_FIXUP_BUNDLE( ) -# Makes a copy of the bundle at location and then fixes up the -# new copied bundle in-place at ... -# -# VERIFY_APP() -# Verifies that an application appears valid based on running analysis -# tools on it. Calls "message(FATAL_ERROR" if the application is not verified. -# -# GET_BUNDLE_MAIN_EXECUTABLE( ) -# The result will be the full path name of the bundle's main executable file -# or an "error:" prefixed string if it could not be determined. -# -# GET_DOTAPP_DIR( ) -# Returns the nearest parent dir whose name ends with ".app" given the full -# path to an executable. If there is no such parent dir, then simply return -# the dir containing the executable. -# -# The returned directory may or may not exist. -# -# GET_BUNDLE_AND_EXECUTABLE( ) -# Takes either a ".app" directory name or the name of an executable -# nested inside a ".app" directory and returns the path to the ".app" -# directory in and the path to its main executable in -# -# -# GET_BUNDLE_ALL_EXECUTABLES( ) -# Scans the given bundle recursively for all executable files and accumulates -# them into a variable. -# -# GET_ITEM_KEY( ) -# Given a file (item) name, generate a key that should be unique considering -# the set of libraries that need copying or fixing up to make a bundle -# standalone. This is essentially the file name including extension with "." -# replaced by "_" -# -# This key is used as a prefix for CMake variables so that we can associate a -# set of variables with a given item based on its key. -# -# CLEAR_BUNDLE_KEYS() -# Loop over the list of keys, clearing all the variables associated with each -# key. After the loop, clear the list of keys itself. -# -# Caller of get_bundle_keys should call clear_bundle_keys when done with list -# of keys. -# -# SET_BUNDLE_KEY_VALUES( -# ) -# Add a key to the list (if necessary) for the given item. If added, -# also set all the variables associated with that key. -# -# GET_BUNDLE_KEYS( ) -# Loop over all the executable and library files within the bundle (and given -# as extra ) and accumulate a list of keys representing them. Set -# values associated with each key such that we can loop over all of them and -# copy prerequisite libs into the bundle and then do appropriate -# install_name_tool fixups. -# -# COPY_RESOLVED_ITEM_INTO_BUNDLE( ) -# Copy a resolved item into the bundle if necessary. Copy is not necessary if -# the resolved_item is "the same as" the resolved_embedded_item. -# -# COPY_RESOLVED_FRAMEWORK_INTO_BUNDLE( ) -# Copy a resolved framework into the bundle if necessary. Copy is not necessary -# if the resolved_item is "the same as" the resolved_embedded_item. -# -# By default, BU_COPY_FULL_FRAMEWORK_CONTENTS is not set. If you want full -# frameworks embedded in your bundles, set BU_COPY_FULL_FRAMEWORK_CONTENTS to -# ON before calling fixup_bundle. By default, -# COPY_RESOLVED_FRAMEWORK_INTO_BUNDLE copies the framework dylib itself plus -# the framework Resources directory. -# -# FIXUP_BUNDLE_ITEM( ) -# Get the direct/non-system prerequisites of the resolved embedded item. For -# each prerequisite, change the way it is referenced to the value of the -# _EMBEDDED_ITEM keyed variable for that prerequisite. (Most likely changing to -# an "@executable_path" style reference.) -# -# This function requires that the resolved_embedded_item be "inside" the bundle -# already. In other words, if you pass plugins to fixup_bundle as the libs -# parameter, you should install them or copy them into the bundle before -# calling fixup_bundle. The "libs" parameter is a list of libraries that must -# be fixed up, but that cannot be determined by otool output analysis. (i.e., -# plugins) -# -# Also, change the id of the item being fixed up to its own _EMBEDDED_ITEM -# value. -# -# Accumulate changes in a local variable and make *one* call to -# install_name_tool at the end of the function with all the changes at once. -# -# If the BU_CHMOD_BUNDLE_ITEMS variable is set then bundle items will be -# marked writable before install_name_tool tries to change them. -# -# VERIFY_BUNDLE_PREREQUISITES( ) -# Verifies that the sum of all prerequisites of all files inside the bundle -# are contained within the bundle or are "system" libraries, presumed to exist -# everywhere. -# -# VERIFY_BUNDLE_SYMLINKS( ) -# Verifies that any symlinks found in the bundle point to other files that are -# already also in the bundle... Anything that points to an external file causes -# this function to fail the verification. - -#============================================================================= -# Copyright 2008-2009 Kitware, Inc. -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distribute this file outside of CMake, substitute the full -# License text for the above reference.) - -# The functions defined in this file depend on the get_prerequisites function -# (and possibly others) found in: -# -get_filename_component(BundleUtilities_cmake_dir "${CMAKE_CURRENT_LIST_FILE}" PATH) -include("${BundleUtilities_cmake_dir}/GetPrerequisites.cmake") - - -function(get_bundle_main_executable bundle result_var) - set(result "error: '${bundle}/Contents/Info.plist' file does not exist") - - if(EXISTS "${bundle}/Contents/Info.plist") - set(result "error: no CFBundleExecutable in '${bundle}/Contents/Info.plist' file") - set(line_is_main_executable 0) - set(bundle_executable "") - - # Read Info.plist as a list of lines: - # - set(eol_char "E") - file(READ "${bundle}/Contents/Info.plist" info_plist) - string(REGEX REPLACE ";" "\\\\;" info_plist "${info_plist}") - string(REGEX REPLACE "\n" "${eol_char};" info_plist "${info_plist}") - - # Scan the lines for "CFBundleExecutable" - the line after that - # is the name of the main executable. - # - foreach(line ${info_plist}) - if(line_is_main_executable) - string(REGEX REPLACE "^.*(.*).*$" "\\1" bundle_executable "${line}") - break() - endif() - - if(line MATCHES "^.*CFBundleExecutable.*$") - set(line_is_main_executable 1) - endif() - endforeach() - - if(NOT "${bundle_executable}" STREQUAL "") - if(EXISTS "${bundle}/Contents/MacOS/${bundle_executable}") - set(result "${bundle}/Contents/MacOS/${bundle_executable}") - else() - - # Ultimate goal: - # If not in "Contents/MacOS" then scan the bundle for matching files. If - # there is only one executable file that matches, then use it, otherwise - # it's an error... - # - #file(GLOB_RECURSE file_list "${bundle}/${bundle_executable}") - - # But for now, pragmatically, it's an error. Expect the main executable - # for the bundle to be in Contents/MacOS, it's an error if it's not: - # - set(result "error: '${bundle}/Contents/MacOS/${bundle_executable}' does not exist") - endif() - endif() - else() - # - # More inclusive technique... (This one would work on Windows and Linux - # too, if a developer followed the typical Mac bundle naming convention...) - # - # If there is no Info.plist file, try to find an executable with the same - # base name as the .app directory: - # - endif() - - set(${result_var} "${result}" PARENT_SCOPE) -endfunction() - - -function(get_dotapp_dir exe dotapp_dir_var) - set(s "${exe}") - - if(s MATCHES "^.*/.*\\.app/.*$") - # If there is a ".app" parent directory, - # ascend until we hit it: - # (typical of a Mac bundle executable) - # - set(done 0) - while(NOT ${done}) - get_filename_component(snamewe "${s}" NAME_WE) - get_filename_component(sname "${s}" NAME) - get_filename_component(sdir "${s}" PATH) - set(s "${sdir}") - if(sname MATCHES "\\.app$") - set(done 1) - set(dotapp_dir "${sdir}/${sname}") - endif() - endwhile() - else() - # Otherwise use a directory containing the exe - # (typical of a non-bundle executable on Mac, Windows or Linux) - # - is_file_executable("${s}" is_executable) - if(is_executable) - get_filename_component(sdir "${s}" PATH) - set(dotapp_dir "${sdir}") - else() - set(dotapp_dir "${s}") - endif() - endif() - - - set(${dotapp_dir_var} "${dotapp_dir}" PARENT_SCOPE) -endfunction() - - -function(get_bundle_and_executable app bundle_var executable_var valid_var) - set(valid 0) - - if(EXISTS "${app}") - # Is it a directory ending in .app? - if(IS_DIRECTORY "${app}") - if(app MATCHES "\\.app$") - get_bundle_main_executable("${app}" executable) - if(EXISTS "${app}" AND EXISTS "${executable}") - set(${bundle_var} "${app}" PARENT_SCOPE) - set(${executable_var} "${executable}" PARENT_SCOPE) - set(valid 1) - #message(STATUS "info: handled .app directory case...") - else() - message(STATUS "warning: *NOT* handled - .app directory case...") - endif() - else() - message(STATUS "warning: *NOT* handled - directory but not .app case...") - endif() - else() - # Is it an executable file? - is_file_executable("${app}" is_executable) - if(is_executable) - get_dotapp_dir("${app}" dotapp_dir) - if(EXISTS "${dotapp_dir}") - set(${bundle_var} "${dotapp_dir}" PARENT_SCOPE) - set(${executable_var} "${app}" PARENT_SCOPE) - set(valid 1) - #message(STATUS "info: handled executable file in .app dir case...") - else() - get_filename_component(app_dir "${app}" PATH) - set(${bundle_var} "${app_dir}" PARENT_SCOPE) - set(${executable_var} "${app}" PARENT_SCOPE) - set(valid 1) - #message(STATUS "info: handled executable file in any dir case...") - endif() - else() - message(STATUS "warning: *NOT* handled - not .app dir, not executable file...") - endif() - endif() - else() - message(STATUS "warning: *NOT* handled - directory/file ${app} does not exist...") - endif() - - if(NOT valid) - set(${bundle_var} "error: not a bundle" PARENT_SCOPE) - set(${executable_var} "error: not a bundle" PARENT_SCOPE) - endif() - - set(${valid_var} ${valid} PARENT_SCOPE) -endfunction() - - -function(get_bundle_all_executables bundle exes_var) - set(exes "") - - file(GLOB_RECURSE file_list "${bundle}/*") - foreach(f ${file_list}) - is_file_executable("${f}" is_executable) - if(is_executable) - set(exes ${exes} "${f}") - endif() - endforeach() - - set(${exes_var} "${exes}" PARENT_SCOPE) -endfunction() - - -function(get_item_key item key_var) - get_filename_component(item_name "${item}" NAME) - if(WIN32) - string(TOLOWER "${item_name}" item_name) - endif() - string(REGEX REPLACE "\\." "_" ${key_var} "${item_name}") - set(${key_var} ${${key_var}} PARENT_SCOPE) -endfunction() - - -function(clear_bundle_keys keys_var) - foreach(key ${${keys_var}}) - set(${key}_ITEM PARENT_SCOPE) - set(${key}_RESOLVED_ITEM PARENT_SCOPE) - set(${key}_DEFAULT_EMBEDDED_PATH PARENT_SCOPE) - set(${key}_EMBEDDED_ITEM PARENT_SCOPE) - set(${key}_RESOLVED_EMBEDDED_ITEM PARENT_SCOPE) - set(${key}_COPYFLAG PARENT_SCOPE) - endforeach() - set(${keys_var} PARENT_SCOPE) -endfunction() - - -function(set_bundle_key_values keys_var context item exepath dirs copyflag) - get_filename_component(item_name "${item}" NAME) - - get_item_key("${item}" key) - - list(LENGTH ${keys_var} length_before) - gp_append_unique(${keys_var} "${key}") - list(LENGTH ${keys_var} length_after) - - if(NOT length_before EQUAL length_after) - gp_resolve_item("${context}" "${item}" "${exepath}" "${dirs}" resolved_item) - - gp_item_default_embedded_path("${item}" default_embedded_path) - - if(item MATCHES "[^/]+\\.framework/") - # For frameworks, construct the name under the embedded path from the - # opening "${item_name}.framework/" to the closing "/${item_name}": - # - string(REGEX REPLACE "^.*(${item_name}.framework/.*/?${item_name}).*$" "${default_embedded_path}/\\1" embedded_item "${item}") - else() - # For other items, just use the same name as the original, but in the - # embedded path: - # - set(embedded_item "${default_embedded_path}/${item_name}") - endif() - - # Replace @executable_path and resolve ".." references: - # - string(REPLACE "@executable_path" "${exepath}" resolved_embedded_item "${embedded_item}") - get_filename_component(resolved_embedded_item "${resolved_embedded_item}" ABSOLUTE) - - # *But* -- if we are not copying, then force resolved_embedded_item to be - # the same as resolved_item. In the case of multiple executables in the - # original bundle, using the default_embedded_path results in looking for - # the resolved executable next to the main bundle executable. This is here - # so that exes in the other sibling directories (like "bin") get fixed up - # properly... - # - if(NOT copyflag) - set(resolved_embedded_item "${resolved_item}") - endif() - - set(${keys_var} ${${keys_var}} PARENT_SCOPE) - set(${key}_ITEM "${item}" PARENT_SCOPE) - set(${key}_RESOLVED_ITEM "${resolved_item}" PARENT_SCOPE) - set(${key}_DEFAULT_EMBEDDED_PATH "${default_embedded_path}" PARENT_SCOPE) - set(${key}_EMBEDDED_ITEM "${embedded_item}" PARENT_SCOPE) - set(${key}_RESOLVED_EMBEDDED_ITEM "${resolved_embedded_item}" PARENT_SCOPE) - set(${key}_COPYFLAG "${copyflag}" PARENT_SCOPE) - else() - #message("warning: item key '${key}' already in the list, subsequent references assumed identical to first") - endif() -endfunction() - - -function(get_bundle_keys app libs dirs keys_var) - set(${keys_var} PARENT_SCOPE) - - get_bundle_and_executable("${app}" bundle executable valid) - if(valid) - # Always use the exepath of the main bundle executable for @executable_path - # replacements: - # - get_filename_component(exepath "${executable}" PATH) - - # But do fixups on all executables in the bundle: - # - get_bundle_all_executables("${bundle}" exes) - - # For each extra lib, accumulate a key as well and then also accumulate - # any of its prerequisites. (Extra libs are typically dynamically loaded - # plugins: libraries that are prerequisites for full runtime functionality - # but that do not show up in otool -L output...) - # - foreach(lib ${libs}) - set_bundle_key_values(${keys_var} "${lib}" "${lib}" "${exepath}" "${dirs}" 0) - - set(prereqs "") - get_prerequisites("${lib}" prereqs 1 1 "${exepath}" "${dirs}") - foreach(pr ${prereqs}) - set_bundle_key_values(${keys_var} "${lib}" "${pr}" "${exepath}" "${dirs}" 1) - endforeach() - endforeach() - - # For each executable found in the bundle, accumulate keys as we go. - # The list of keys should be complete when all prerequisites of all - # binaries in the bundle have been analyzed. - # - foreach(exe ${exes}) - # Add the exe itself to the keys: - # - set_bundle_key_values(${keys_var} "${exe}" "${exe}" "${exepath}" "${dirs}" 0) - - # Add each prerequisite to the keys: - # - set(prereqs "") - get_prerequisites("${exe}" prereqs 1 1 "${exepath}" "${dirs}") - foreach(pr ${prereqs}) - set_bundle_key_values(${keys_var} "${exe}" "${pr}" "${exepath}" "${dirs}" 1) - endforeach() - endforeach() - - # Propagate values to caller's scope: - # - set(${keys_var} ${${keys_var}} PARENT_SCOPE) - foreach(key ${${keys_var}}) - set(${key}_ITEM "${${key}_ITEM}" PARENT_SCOPE) - set(${key}_RESOLVED_ITEM "${${key}_RESOLVED_ITEM}" PARENT_SCOPE) - set(${key}_DEFAULT_EMBEDDED_PATH "${${key}_DEFAULT_EMBEDDED_PATH}" PARENT_SCOPE) - set(${key}_EMBEDDED_ITEM "${${key}_EMBEDDED_ITEM}" PARENT_SCOPE) - set(${key}_RESOLVED_EMBEDDED_ITEM "${${key}_RESOLVED_EMBEDDED_ITEM}" PARENT_SCOPE) - set(${key}_COPYFLAG "${${key}_COPYFLAG}" PARENT_SCOPE) - endforeach() - endif() -endfunction() - - -function(copy_resolved_item_into_bundle resolved_item resolved_embedded_item) - if(WIN32) - # ignore case on Windows - string(TOLOWER "${resolved_item}" resolved_item_compare) - string(TOLOWER "${resolved_embedded_item}" resolved_embedded_item_compare) - else() - set(resolved_item_compare "${resolved_item}") - set(resolved_embedded_item_compare "${resolved_embedded_item}") - endif() - - if("${resolved_item_compare}" STREQUAL "${resolved_embedded_item_compare}") - message(STATUS "warning: resolved_item == resolved_embedded_item - not copying...") - else() - #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy ${resolved_item} ${resolved_embedded_item}") - execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_item}" "${resolved_embedded_item}") - if(UNIX AND NOT APPLE) - file(RPATH_REMOVE FILE "${resolved_embedded_item}") - endif() - endif() - -endfunction() - - -function(copy_resolved_framework_into_bundle resolved_item resolved_embedded_item) - if(WIN32) - # ignore case on Windows - string(TOLOWER "${resolved_item}" resolved_item_compare) - string(TOLOWER "${resolved_embedded_item}" resolved_embedded_item_compare) - else() - set(resolved_item_compare "${resolved_item}") - set(resolved_embedded_item_compare "${resolved_embedded_item}") - endif() - - if("${resolved_item_compare}" STREQUAL "${resolved_embedded_item_compare}") - message(STATUS "warning: resolved_item == resolved_embedded_item - not copying...") - else() - if(BU_COPY_FULL_FRAMEWORK_CONTENTS) - # Full Framework (everything): - get_filename_component(resolved_dir "${resolved_item}" PATH) - get_filename_component(resolved_dir "${resolved_dir}/../.." ABSOLUTE) - get_filename_component(resolved_embedded_dir "${resolved_embedded_item}" PATH) - get_filename_component(resolved_embedded_dir "${resolved_embedded_dir}/../.." ABSOLUTE) - #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy_directory '${resolved_dir}' '${resolved_embedded_dir}'") - execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${resolved_dir}" "${resolved_embedded_dir}") - else() - # Framework lib itself: - #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy ${resolved_item} ${resolved_embedded_item}") - execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_item}" "${resolved_embedded_item}") - - # Plus Resources, if they exist: - string(REGEX REPLACE "^(.*)/[^/]+/[^/]+/[^/]+$" "\\1/Resources" resolved_resources "${resolved_item}") - string(REGEX REPLACE "^(.*)/[^/]+/[^/]+/[^/]+$" "\\1/Resources" resolved_embedded_resources "${resolved_embedded_item}") - if(EXISTS "${resolved_resources}") - #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy_directory '${resolved_resources}' '${resolved_embedded_resources}'") - execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${resolved_resources}" "${resolved_embedded_resources}") - endif() - endif() - if(UNIX AND NOT APPLE) - file(RPATH_REMOVE FILE "${resolved_embedded_item}") - endif() - endif() - -endfunction() - - -function(fixup_bundle_item resolved_embedded_item exepath dirs) - # This item's key is "ikey": - # - get_item_key("${resolved_embedded_item}" ikey) - - # Ensure the item is "inside the .app bundle" -- it should not be fixed up if - # it is not in the .app bundle... Otherwise, we'll modify files in the build - # tree, or in other varied locations around the file system, with our call to - # install_name_tool. Make sure that doesn't happen here: - # - get_dotapp_dir("${exepath}" exe_dotapp_dir) - string(LENGTH "${exe_dotapp_dir}/" exe_dotapp_dir_length) - string(LENGTH "${resolved_embedded_item}" resolved_embedded_item_length) - set(path_too_short 0) - set(is_embedded 0) - if(${resolved_embedded_item_length} LESS ${exe_dotapp_dir_length}) - set(path_too_short 1) - endif() - if(NOT path_too_short) - string(SUBSTRING "${resolved_embedded_item}" 0 ${exe_dotapp_dir_length} item_substring) - if("${exe_dotapp_dir}/" STREQUAL "${item_substring}") - set(is_embedded 1) - endif() - endif() - if(NOT is_embedded) - message(" exe_dotapp_dir/='${exe_dotapp_dir}/'") - message(" item_substring='${item_substring}'") - message(" resolved_embedded_item='${resolved_embedded_item}'") - message("") - message("Install or copy the item into the bundle before calling fixup_bundle.") - message("Or maybe there's a typo or incorrect path in one of the args to fixup_bundle?") - message("") - message(FATAL_ERROR "cannot fixup an item that is not in the bundle...") - endif() - - set(prereqs "") - get_prerequisites("${resolved_embedded_item}" prereqs 1 0 "${exepath}" "${dirs}") - - set(changes "") - - foreach(pr ${prereqs}) - # Each referenced item's key is "rkey" in the loop: - # - get_item_key("${pr}" rkey) - - if(NOT "${${rkey}_EMBEDDED_ITEM}" STREQUAL "") - set(changes ${changes} "-change" "${pr}" "${${rkey}_EMBEDDED_ITEM}") - else() - message("warning: unexpected reference to '${pr}'") - endif() - endforeach() - - if(BU_CHMOD_BUNDLE_ITEMS) - execute_process(COMMAND chmod u+w "${resolved_embedded_item}") - endif() - - # Change this item's id and all of its references in one call - # to install_name_tool: - # - execute_process(COMMAND install_name_tool - ${changes} -id "${${ikey}_EMBEDDED_ITEM}" "${resolved_embedded_item}" - ) -endfunction() - - -function(fixup_bundle app libs dirs) - message(STATUS "fixup_bundle") - message(STATUS " app='${app}'") - message(STATUS " libs='${libs}'") - message(STATUS " dirs='${dirs}'") - - get_bundle_and_executable("${app}" bundle executable valid) - if(valid) - get_filename_component(exepath "${executable}" PATH) - - message(STATUS "fixup_bundle: preparing...") - get_bundle_keys("${app}" "${libs}" "${dirs}" keys) - - message(STATUS "fixup_bundle: copying...") - list(LENGTH keys n) - math(EXPR n ${n}*2) - - set(i 0) - foreach(key ${keys}) - math(EXPR i ${i}+1) - if(${${key}_COPYFLAG}) - message(STATUS "${i}/${n}: copying '${${key}_RESOLVED_ITEM}'") - else() - message(STATUS "${i}/${n}: *NOT* copying '${${key}_RESOLVED_ITEM}'") - endif() - - set(show_status 0) - if(show_status) - message(STATUS "key='${key}'") - message(STATUS "item='${${key}_ITEM}'") - message(STATUS "resolved_item='${${key}_RESOLVED_ITEM}'") - message(STATUS "default_embedded_path='${${key}_DEFAULT_EMBEDDED_PATH}'") - message(STATUS "embedded_item='${${key}_EMBEDDED_ITEM}'") - message(STATUS "resolved_embedded_item='${${key}_RESOLVED_EMBEDDED_ITEM}'") - message(STATUS "copyflag='${${key}_COPYFLAG}'") - message(STATUS "") - endif() - - if(${${key}_COPYFLAG}) - set(item "${${key}_ITEM}") - if(item MATCHES "[^/]+\\.framework/") - copy_resolved_framework_into_bundle("${${key}_RESOLVED_ITEM}" - "${${key}_RESOLVED_EMBEDDED_ITEM}") - else() - copy_resolved_item_into_bundle("${${key}_RESOLVED_ITEM}" - "${${key}_RESOLVED_EMBEDDED_ITEM}") - endif() - endif() - endforeach() - - message(STATUS "fixup_bundle: fixing...") - foreach(key ${keys}) - math(EXPR i ${i}+1) - if(APPLE) - message(STATUS "${i}/${n}: fixing up '${${key}_RESOLVED_EMBEDDED_ITEM}'") - fixup_bundle_item("${${key}_RESOLVED_EMBEDDED_ITEM}" "${exepath}" "${dirs}") - else() - message(STATUS "${i}/${n}: fix-up not required on this platform '${${key}_RESOLVED_EMBEDDED_ITEM}'") - endif() - endforeach() - - message(STATUS "fixup_bundle: cleaning up...") - clear_bundle_keys(keys) - - message(STATUS "fixup_bundle: verifying...") - verify_app("${app}") - else() - message(SEND_ERROR "error: fixup_bundle: not a valid bundle") - endif() - - message(STATUS "fixup_bundle: done") -endfunction() - - -function(copy_and_fixup_bundle src dst libs dirs) - execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${src}" "${dst}") - fixup_bundle("${dst}" "${libs}" "${dirs}") -endfunction() - - -function(verify_bundle_prerequisites bundle result_var info_var) - set(result 1) - set(info "") - set(count 0) - - get_bundle_main_executable("${bundle}" main_bundle_exe) - - file(GLOB_RECURSE file_list "${bundle}/*") - foreach(f ${file_list}) - is_file_executable("${f}" is_executable) - if(is_executable) - get_filename_component(exepath "${f}" PATH) - math(EXPR count "${count} + 1") - - message(STATUS "executable file ${count}: ${f}") - - set(prereqs "") - get_prerequisites("${f}" prereqs 1 1 "${exepath}" "") - - # On the Mac, - # "embedded" and "system" prerequisites are fine... anything else means - # the bundle's prerequisites are not verified (i.e., the bundle is not - # really "standalone") - # - # On Windows (and others? Linux/Unix/...?) - # "local" and "system" prereqs are fine... - # - set(external_prereqs "") - - foreach(p ${prereqs}) - set(p_type "") - gp_file_type("${f}" "${p}" p_type) - - if(APPLE) - if(NOT "${p_type}" STREQUAL "embedded" AND NOT "${p_type}" STREQUAL "system") - set(external_prereqs ${external_prereqs} "${p}") - endif() - else() - if(NOT "${p_type}" STREQUAL "local" AND NOT "${p_type}" STREQUAL "system") - set(external_prereqs ${external_prereqs} "${p}") - endif() - endif() - endforeach() - - if(external_prereqs) - # Found non-system/somehow-unacceptable prerequisites: - set(result 0) - set(info ${info} "external prerequisites found:\nf='${f}'\nexternal_prereqs='${external_prereqs}'\n") - endif() - endif() - endforeach() - - if(result) - set(info "Verified ${count} executable files in '${bundle}'") - endif() - - set(${result_var} "${result}" PARENT_SCOPE) - set(${info_var} "${info}" PARENT_SCOPE) -endfunction() - - -function(verify_bundle_symlinks bundle result_var info_var) - set(result 1) - set(info "") - set(count 0) - - # TODO: implement this function for real... - # Right now, it is just a stub that verifies unconditionally... - - set(${result_var} "${result}" PARENT_SCOPE) - set(${info_var} "${info}" PARENT_SCOPE) -endfunction() - - -function(verify_app app) - set(verified 0) - set(info "") - - get_bundle_and_executable("${app}" bundle executable valid) - - message(STATUS "===========================================================================") - message(STATUS "Analyzing app='${app}'") - message(STATUS "bundle='${bundle}'") - message(STATUS "executable='${executable}'") - message(STATUS "valid='${valid}'") - - # Verify that the bundle does not have any "external" prerequisites: - # - verify_bundle_prerequisites("${bundle}" verified info) - message(STATUS "verified='${verified}'") - message(STATUS "info='${info}'") - message(STATUS "") - - if(verified) - # Verify that the bundle does not have any symlinks to external files: - # - verify_bundle_symlinks("${bundle}" verified info) - message(STATUS "verified='${verified}'") - message(STATUS "info='${info}'") - message(STATUS "") - endif() - - if(NOT verified) - message(FATAL_ERROR "error: verify_app failed") - endif() -endfunction() diff --git a/cmake/GetPrerequisites.cmake b/cmake/GetPrerequisites.cmake deleted file mode 100644 index 39c2cc63..00000000 --- a/cmake/GetPrerequisites.cmake +++ /dev/null @@ -1,902 +0,0 @@ -# - Functions to analyze and list executable file prerequisites. -# This module provides functions to list the .dll, .dylib or .so -# files that an executable or shared library file depends on. (Its -# prerequisites.) -# -# It uses various tools to obtain the list of required shared library files: -# dumpbin (Windows) -# objdump (MinGW on Windows) -# ldd (Linux/Unix) -# otool (Mac OSX) -# The following functions are provided by this module: -# get_prerequisites -# list_prerequisites -# list_prerequisites_by_glob -# gp_append_unique -# is_file_executable -# gp_item_default_embedded_path -# (projects can override with gp_item_default_embedded_path_override) -# gp_resolve_item -# (projects can override with gp_resolve_item_override) -# gp_resolved_file_type -# (projects can override with gp_resolved_file_type_override) -# gp_file_type -# Requires CMake 2.6 or greater because it uses function, break, return and -# PARENT_SCOPE. -# -# GET_PREREQUISITES( -# ) -# Get the list of shared library files required by . The list in -# the variable named should be empty on first entry to -# this function. On exit, will contain the list of -# required shared library files. -# -# is the full path to an executable file. is the -# name of a CMake variable to contain the results. must be 0 -# or 1 indicating whether to include or exclude "system" prerequisites. If -# is set to 1 all prerequisites will be found recursively, if set to -# 0 only direct prerequisites are listed. is the path to the top -# level executable used for @executable_path replacment on the Mac. is -# a list of paths where libraries might be found: these paths are searched -# first when a target without any path info is given. Then standard system -# locations are also searched: PATH, Framework locations, /usr/lib... -# -# LIST_PREREQUISITES( [ [ []]]) -# Print a message listing the prerequisites of . -# -# is the name of a shared library or executable target or the full -# path to a shared library or executable file. If is set to 1 all -# prerequisites will be found recursively, if set to 0 only direct -# prerequisites are listed. must be 0 or 1 indicating whether -# to include or exclude "system" prerequisites. With set to 0 only -# the full path names of the prerequisites are printed, set to 1 extra -# informatin will be displayed. -# -# LIST_PREREQUISITES_BY_GLOB( ) -# Print the prerequisites of shared library and executable files matching a -# globbing pattern. is GLOB or GLOB_RECURSE and is a -# globbing expression used with "file(GLOB" or "file(GLOB_RECURSE" to retrieve -# a list of matching files. If a matching file is executable, its prerequisites -# are listed. -# -# Any additional (optional) arguments provided are passed along as the -# optional arguments to the list_prerequisites calls. -# -# GP_APPEND_UNIQUE( ) -# Append to the list variable only if the value is not -# already in the list. -# -# IS_FILE_EXECUTABLE( ) -# Return 1 in if is a binary executable, 0 otherwise. -# -# GP_ITEM_DEFAULT_EMBEDDED_PATH( ) -# Return the path that others should refer to the item by when the item -# is embedded inside a bundle. -# -# Override on a per-project basis by providing a project-specific -# gp_item_default_embedded_path_override function. -# -# GP_RESOLVE_ITEM( ) -# Resolve an item into an existing full path file. -# -# Override on a per-project basis by providing a project-specific -# gp_resolve_item_override function. -# -# GP_RESOLVED_FILE_TYPE( ) -# Return the type of with respect to . String -# describing type of prerequisite is returned in variable named . -# -# Use and if necessary to resolve non-absolute -# values -- but only for non-embedded items. -# -# Possible types are: -# system -# local -# embedded -# other -# Override on a per-project basis by providing a project-specific -# gp_resolved_file_type_override function. -# -# GP_FILE_TYPE( ) -# Return the type of with respect to . String -# describing type of prerequisite is returned in variable named . -# -# Possible types are: -# system -# local -# embedded -# other - -#============================================================================= -# Copyright 2008-2009 Kitware, Inc. -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distribute this file outside of CMake, substitute the full -# License text for the above reference.) - -function(gp_append_unique list_var value) - set(contains 0) - - foreach(item ${${list_var}}) - if("${item}" STREQUAL "${value}") - set(contains 1) - break() - endif() - endforeach() - - if(NOT contains) - set(${list_var} ${${list_var}} "${value}" PARENT_SCOPE) - endif() -endfunction() - - -function(is_file_executable file result_var) - # - # A file is not executable until proven otherwise: - # - set(${result_var} 0 PARENT_SCOPE) - - get_filename_component(file_full "${file}" ABSOLUTE) - string(TOLOWER "${file_full}" file_full_lower) - - # If file name ends in .exe on Windows, *assume* executable: - # - if(WIN32 AND NOT UNIX) - if("${file_full_lower}" MATCHES "\\.exe$") - set(${result_var} 1 PARENT_SCOPE) - return() - endif() - - # A clause could be added here that uses output or return value of dumpbin - # to determine ${result_var}. In 99%+? practical cases, the exe name - # match will be sufficient... - # - endif() - - # Use the information returned from the Unix shell command "file" to - # determine if ${file_full} should be considered an executable file... - # - # If the file command's output contains "executable" and does *not* contain - # "text" then it is likely an executable suitable for prerequisite analysis - # via the get_prerequisites macro. - # - if(UNIX) - if(NOT file_cmd) - find_program(file_cmd "file") - mark_as_advanced(file_cmd) - endif() - - if(file_cmd) - execute_process(COMMAND "${file_cmd}" "${file_full}" - OUTPUT_VARIABLE file_ov - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - - # Replace the name of the file in the output with a placeholder token - # (the string " _file_full_ ") so that just in case the path name of - # the file contains the word "text" or "executable" we are not fooled - # into thinking "the wrong thing" because the file name matches the - # other 'file' command output we are looking for... - # - string(REPLACE "${file_full}" " _file_full_ " file_ov "${file_ov}") - string(TOLOWER "${file_ov}" file_ov) - - #message(STATUS "file_ov='${file_ov}'") - if("${file_ov}" MATCHES "executable") - #message(STATUS "executable!") - if("${file_ov}" MATCHES "text") - #message(STATUS "but text, so *not* a binary executable!") - else() - set(${result_var} 1 PARENT_SCOPE) - return() - endif() - endif() - - # Also detect position independent executables on Linux, - # where "file" gives "shared object ... (uses shared libraries)" - if("${file_ov}" MATCHES "shared object.*\(uses shared libs\)") - set(${result_var} 1 PARENT_SCOPE) - return() - endif() - - # "file" version 5.22 does not print "(used shared libraries)" - # but uses "interpreter" - if("${file_ov}" MATCHES "shared object.*interpreter") - set(${result_var} 1 PARENT_SCOPE) - return() - endif() - - else() - message(STATUS "warning: No 'file' command, skipping execute_process...") - endif() - endif() -endfunction() - - -function(gp_item_default_embedded_path item default_embedded_path_var) - - # On Windows and Linux, "embed" prerequisites in the same directory - # as the executable by default: - # - set(path "@executable_path") - set(overridden 0) - - # On the Mac, relative to the executable depending on the type - # of the thing we are embedding: - # - if(APPLE) - # - # The assumption here is that all executables in the bundle will be - # in same-level-directories inside the bundle. The parent directory - # of an executable inside the bundle should be MacOS or a sibling of - # MacOS and all embedded paths returned from here will begin with - # "@executable_path/../" and will work from all executables in all - # such same-level-directories inside the bundle. - # - - # By default, embed things right next to the main bundle executable: - # - set(path "@executable_path/../../Contents/MacOS") - - # Embed .dylibs right next to the main bundle executable: - # - if(item MATCHES "\\.dylib$") - set(path "@executable_path/../MacOS") - set(overridden 1) - endif() - - # Embed frameworks in the embedded "Frameworks" directory (sibling of MacOS): - # - if(NOT overridden) - if(item MATCHES "[^/]+\\.framework/") - set(path "@executable_path/../Frameworks") - set(overridden 1) - endif() - endif() - endif() - - # Provide a hook so that projects can override the default embedded location - # of any given library by whatever logic they choose: - # - if(COMMAND gp_item_default_embedded_path_override) - gp_item_default_embedded_path_override("${item}" path) - endif() - - set(${default_embedded_path_var} "${path}" PARENT_SCOPE) -endfunction() - - -function(gp_resolve_item context item exepath dirs resolved_item_var) - set(resolved 0) - set(resolved_item "${item}") - - # Is it already resolved? - # - if(IS_ABSOLUTE "${resolved_item}" AND EXISTS "${resolved_item}") - set(resolved 1) - endif() - - if(NOT resolved) - if(item MATCHES "@executable_path") - # - # @executable_path references are assumed relative to exepath - # - string(REPLACE "@executable_path" "${exepath}" ri "${item}") - get_filename_component(ri "${ri}" ABSOLUTE) - - if(EXISTS "${ri}") - #message(STATUS "info: embedded item exists (${ri})") - set(resolved 1) - set(resolved_item "${ri}") - else() - message(STATUS "warning: embedded item does not exist '${ri}'") - endif() - endif() - endif() - - if(NOT resolved) - if(item MATCHES "@loader_path") - # - # @loader_path references are assumed relative to the - # PATH of the given "context" (presumably another library) - # - get_filename_component(contextpath "${context}" PATH) - string(REPLACE "@loader_path" "${contextpath}" ri "${item}") - get_filename_component(ri "${ri}" ABSOLUTE) - - if(EXISTS "${ri}") - #message(STATUS "info: embedded item exists (${ri})") - set(resolved 1) - set(resolved_item "${ri}") - else() - message(STATUS "warning: embedded item does not exist '${ri}'") - endif() - endif() - endif() - - if(NOT resolved) - if(item MATCHES "@rpath") - # - # @rpath references are relative to the paths built into the binaries with -rpath - # We handle this case like we do for other Unixes - # - string(REPLACE "@rpath/" "" norpath_item "${item}") - - set(ri "ri-NOTFOUND") - find_file(ri "${norpath_item}" ${exepath} ${dirs} NO_DEFAULT_PATH) - if(ri) - #message(STATUS "info: 'find_file' in exepath/dirs (${ri})") - set(resolved 1) - set(resolved_item "${ri}") - set(ri "ri-NOTFOUND") - endif() - - endif() - endif() - - if(NOT resolved) - set(ri "ri-NOTFOUND") - find_file(ri "${item}" ${exepath} ${dirs} NO_DEFAULT_PATH) - find_file(ri "${item}" ${exepath} ${dirs} /usr/lib) - if(ri) - #message(STATUS "info: 'find_file' in exepath/dirs (${ri})") - set(resolved 1) - set(resolved_item "${ri}") - set(ri "ri-NOTFOUND") - endif() - endif() - - if(NOT resolved) - if(item MATCHES "[^/]+\\.framework/") - set(fw "fw-NOTFOUND") - find_file(fw "${item}" - "~/Library/Frameworks" - "/Library/Frameworks" - "/System/Library/Frameworks" - ) - if(fw) - #message(STATUS "info: 'find_file' found framework (${fw})") - set(resolved 1) - set(resolved_item "${fw}") - set(fw "fw-NOTFOUND") - endif() - endif() - endif() - - # Using find_program on Windows will find dll files that are in the PATH. - # (Converting simple file names into full path names if found.) - # - if(WIN32 AND NOT UNIX) - if(NOT resolved) - set(ri "ri-NOTFOUND") - find_program(ri "${item}" PATHS "${exepath};${dirs}" NO_DEFAULT_PATH) - find_program(ri "${item}" PATHS "${exepath};${dirs}") - if(ri) - #message(STATUS "info: 'find_program' in exepath/dirs (${ri})") - set(resolved 1) - set(resolved_item "${ri}") - set(ri "ri-NOTFOUND") - endif() - endif() - endif() - - # Provide a hook so that projects can override item resolution - # by whatever logic they choose: - # - if(COMMAND gp_resolve_item_override) - gp_resolve_item_override("${context}" "${item}" "${exepath}" "${dirs}" resolved_item resolved) - endif() - - if(NOT resolved) - message(STATUS " -warning: cannot resolve item '${item}' - - possible problems: - need more directories? - need to use InstallRequiredSystemLibraries? - run in install tree instead of build tree? -") -# message(STATUS " -#****************************************************************************** -#warning: cannot resolve item '${item}' -# -# possible problems: -# need more directories? -# need to use InstallRequiredSystemLibraries? -# run in install tree instead of build tree? -# -# context='${context}' -# item='${item}' -# exepath='${exepath}' -# dirs='${dirs}' -# resolved_item_var='${resolved_item_var}' -#****************************************************************************** -#") - endif() - - set(${resolved_item_var} "${resolved_item}" PARENT_SCOPE) -endfunction() - - -function(gp_resolved_file_type original_file file exepath dirs type_var) - #message(STATUS "**") - - if(NOT IS_ABSOLUTE "${original_file}") - message(STATUS "warning: gp_resolved_file_type expects absolute full path for first arg original_file") - endif() - - set(is_embedded 0) - set(is_local 0) - set(is_system 0) - - set(resolved_file "${file}") - - if("${file}" MATCHES "^@(executable|loader)_path") - set(is_embedded 1) - endif() - - if(NOT is_embedded) - if(NOT IS_ABSOLUTE "${file}") - gp_resolve_item("${original_file}" "${file}" "${exepath}" "${dirs}" resolved_file) - endif() - - string(TOLOWER "${original_file}" original_lower) - string(TOLOWER "${resolved_file}" lower) - - if(UNIX) - if(resolved_file MATCHES "^(/lib/|/lib32/|/lib64/|/usr/lib/|/usr/lib32/|/usr/lib64/|/usr/X11R6/|/usr/bin/)") - set(is_system 1) - endif() - endif() - - if(APPLE) - if(resolved_file MATCHES "^(/System/Library/|/usr/lib/)") - set(is_system 1) - endif() - endif() - - if(WIN32) - string(TOLOWER "$ENV{SystemRoot}" sysroot) - string(REGEX REPLACE "\\\\" "/" sysroot "${sysroot}") - - string(TOLOWER "$ENV{windir}" windir) - string(REGEX REPLACE "\\\\" "/" windir "${windir}") - - if(lower MATCHES "^(${sysroot}/sys(tem|wow)|${windir}/sys(tem|wow)|(.*/)*msvc[^/]+dll)") - set(is_system 1) - endif() - - if(UNIX) - # if cygwin, we can get the properly formed windows paths from cygpath - find_program(CYGPATH_EXECUTABLE cygpath) - - if(CYGPATH_EXECUTABLE) - execute_process(COMMAND ${CYGPATH_EXECUTABLE} -W - OUTPUT_VARIABLE env_windir - OUTPUT_STRIP_TRAILING_WHITESPACE) - execute_process(COMMAND ${CYGPATH_EXECUTABLE} -S - OUTPUT_VARIABLE env_sysdir - OUTPUT_STRIP_TRAILING_WHITESPACE) - string(TOLOWER "${env_windir}" windir) - string(TOLOWER "${env_sysdir}" sysroot) - - if(lower MATCHES "^(${sysroot}/sys(tem|wow)|${windir}/sys(tem|wow)|(.*/)*msvc[^/]+dll)") - set(is_system 1) - endif() - endif() - endif() - endif() - - if(NOT is_system) - get_filename_component(original_path "${original_lower}" PATH) - get_filename_component(path "${lower}" PATH) - if("${original_path}" STREQUAL "${path}") - set(is_local 1) - else() - string(LENGTH "${original_path}/" original_length) - string(LENGTH "${lower}" path_length) - if(${path_length} GREATER ${original_length}) - string(SUBSTRING "${lower}" 0 ${original_length} path) - if("${original_path}/" STREQUAL "${path}") - set(is_embedded 1) - endif() - endif() - endif() - endif() - endif() - - # Return type string based on computed booleans: - # - set(type "other") - - if(is_system) - set(type "system") - elseif(is_embedded) - set(type "embedded") - elseif(is_local) - set(type "local") - endif() - - #message(STATUS "gp_resolved_file_type: '${file}' '${resolved_file}'") - #message(STATUS " type: '${type}'") - - if(NOT is_embedded) - if(NOT IS_ABSOLUTE "${resolved_file}") - if(lower MATCHES "^msvc[^/]+dll" AND is_system) - message(STATUS "info: non-absolute msvc file '${file}' returning type '${type}'") - else() - message(STATUS "warning: gp_resolved_file_type non-absolute file '${file}' returning type '${type}' -- possibly incorrect") - endif() - endif() - endif() - - # Provide a hook so that projects can override the decision on whether a - # library belongs to the system or not by whatever logic they choose: - # - if(COMMAND gp_resolved_file_type_override) - gp_resolved_file_type_override("${resolved_file}" type) - endif() - - set(${type_var} "${type}" PARENT_SCOPE) - - #message(STATUS "**") -endfunction() - - -function(gp_file_type original_file file type_var) - if(NOT IS_ABSOLUTE "${original_file}") - message(STATUS "warning: gp_file_type expects absolute full path for first arg original_file") - endif() - - get_filename_component(exepath "${original_file}" PATH) - - set(type "") - gp_resolved_file_type("${original_file}" "${file}" "${exepath}" "" type) - - set(${type_var} "${type}" PARENT_SCOPE) -endfunction() - - -function(get_prerequisites target prerequisites_var exclude_system recurse exepath dirs) - set(verbose 0) - set(eol_char "E") - - if(NOT IS_ABSOLUTE "${target}") - message("warning: target '${target}' is not absolute...") - endif() - - if(NOT EXISTS "${target}") - message("warning: target '${target}' does not exist...") - endif() - - set(gp_cmd_paths ${gp_cmd_paths} - "C:/Program Files/Microsoft Visual Studio 9.0/VC/bin" - "C:/Program Files (x86)/Microsoft Visual Studio 9.0/VC/bin" - "C:/Program Files/Microsoft Visual Studio 8/VC/BIN" - "C:/Program Files (x86)/Microsoft Visual Studio 8/VC/BIN" - "C:/Program Files/Microsoft Visual Studio .NET 2003/VC7/BIN" - "C:/Program Files (x86)/Microsoft Visual Studio .NET 2003/VC7/BIN" - "/usr/local/bin" - "/usr/bin" - ) - - # - # - # Try to choose the right tool by default. Caller can set gp_tool prior to - # calling this function to force using a different tool. - # - if("${gp_tool}" STREQUAL "") - set(gp_tool "ldd") - - if(APPLE) - set(gp_tool "otool") - endif() - - if(WIN32 AND NOT UNIX) # This is how to check for cygwin, har! - find_program(gp_dumpbin "dumpbin" PATHS ${gp_cmd_paths}) - if(gp_dumpbin) - set(gp_tool "dumpbin") - else() # Try harder. Maybe we're on MinGW - set(gp_tool "objdump") - endif() - endif() - endif() - - find_program(gp_cmd ${gp_tool} PATHS ${gp_cmd_paths}) - - if(NOT gp_cmd) - message(FATAL_ERROR "FATAL ERROR: could not find '${gp_tool}' - cannot analyze prerequisites!") - return() - endif() - - set(gp_tool_known 0) - - if("${gp_tool}" STREQUAL "ldd") - set(gp_cmd_args "") - set(gp_regex "^[\t ]*[^\t ]+ => ([^\t\(]+) .*${eol_char}$") - set(gp_regex_error "not found${eol_char}$") - set(gp_regex_fallback "^[\t ]*([^\t ]+) => ([^\t ]+).*${eol_char}$") - set(gp_regex_cmp_count 1) - set(gp_tool_known 1) - endif() - - if("${gp_tool}" STREQUAL "otool") - set(gp_cmd_args "-L") - set(gp_regex "^\t([^\t]+) \\(compatibility version ([0-9]+.[0-9]+.[0-9]+), current version ([0-9]+.[0-9]+.[0-9]+)\\)${eol_char}$") - set(gp_regex_error "") - set(gp_regex_fallback "") - set(gp_regex_cmp_count 3) - set(gp_tool_known 1) - endif() - - if("${gp_tool}" STREQUAL "dumpbin") - set(gp_cmd_args "/dependents") - set(gp_regex "^ ([^ ].*[Dd][Ll][Ll])${eol_char}$") - set(gp_regex_error "") - set(gp_regex_fallback "") - set(gp_regex_cmp_count 1) - set(gp_tool_known 1) - endif() - - if("${gp_tool}" STREQUAL "objdump") - set(gp_cmd_args "-p") - set(gp_regex "^\t*DLL Name: (.*\\.[Dd][Ll][Ll])${eol_char}$") - set(gp_regex_error "") - set(gp_regex_fallback "") - set(gp_regex_cmp_count 1) - set(gp_tool_known 1) - endif() - - if(NOT gp_tool_known) - message(STATUS "warning: gp_tool='${gp_tool}' is an unknown tool...") - message(STATUS "CMake function get_prerequisites needs more code to handle '${gp_tool}'") - message(STATUS "Valid gp_tool values are dumpbin, ldd, objdump and otool.") - return() - endif() - - - if("${gp_tool}" STREQUAL "dumpbin") - # When running dumpbin, it also needs the "Common7/IDE" directory in the - # PATH. It will already be in the PATH if being run from a Visual Studio - # command prompt. Add it to the PATH here in case we are running from a - # different command prompt. - # - get_filename_component(gp_cmd_dir "${gp_cmd}" PATH) - get_filename_component(gp_cmd_dlls_dir "${gp_cmd_dir}/../../Common7/IDE" ABSOLUTE) - # Use cmake paths as a user may have a PATH element ending with a backslash. - # This will escape the list delimiter and create havoc! - if(EXISTS "${gp_cmd_dlls_dir}") - # only add to the path if it is not already in the path - set(gp_found_cmd_dlls_dir 0) - file(TO_CMAKE_PATH "$ENV{PATH}" env_path) - foreach(gp_env_path_element ${env_path}) - if("${gp_env_path_element}" STREQUAL "${gp_cmd_dlls_dir}") - set(gp_found_cmd_dlls_dir 1) - endif() - endforeach() - - if(NOT gp_found_cmd_dlls_dir) - file(TO_NATIVE_PATH "${gp_cmd_dlls_dir}" gp_cmd_dlls_dir) - set(ENV{PATH} "$ENV{PATH};${gp_cmd_dlls_dir}") - endif() - endif() - endif() - # - # - - if("${gp_tool}" STREQUAL "ldd") - set(old_ld_env "$ENV{LD_LIBRARY_PATH}") - foreach(dir ${exepath} ${dirs}) - set(ENV{LD_LIBRARY_PATH} "${dir}:$ENV{LD_LIBRARY_PATH}") - endforeach() - endif() - - - # Track new prerequisites at each new level of recursion. Start with an - # empty list at each level: - # - set(unseen_prereqs) - - # Run gp_cmd on the target: - # - execute_process( - COMMAND ${gp_cmd} ${gp_cmd_args} ${target} - OUTPUT_VARIABLE gp_cmd_ov - ) - - if("${gp_tool}" STREQUAL "ldd") - set(ENV{LD_LIBRARY_PATH} "${old_ld_env}") - endif() - - if(verbose) - message(STATUS "") - message(STATUS "gp_cmd_ov='${gp_cmd_ov}'") - message(STATUS "") - endif() - - get_filename_component(target_dir "${target}" PATH) - - # Convert to a list of lines: - # - string(REGEX REPLACE ";" "\\\\;" candidates "${gp_cmd_ov}") - string(REGEX REPLACE "\n" "${eol_char};" candidates "${candidates}") - - # check for install id and remove it from list, since otool -L can include a - # reference to itself - set(gp_install_id) - if("${gp_tool}" STREQUAL "otool") - execute_process( - COMMAND otool -D ${target} - OUTPUT_VARIABLE gp_install_id_ov - ) - # second line is install name - string(REGEX REPLACE ".*:\n" "" gp_install_id "${gp_install_id_ov}") - if(gp_install_id) - # trim - string(REGEX MATCH "[^\n ].*[^\n ]" gp_install_id "${gp_install_id}") - #message("INSTALL ID is \"${gp_install_id}\"") - endif() - endif() - - # Analyze each line for file names that match the regular expression: - # - foreach(candidate ${candidates}) - if("${candidate}" MATCHES "${gp_regex}") - - # Extract information from each candidate: - if(gp_regex_error AND "${candidate}" MATCHES "${gp_regex_error}") - string(REGEX REPLACE "${gp_regex_fallback}" "\\1" raw_item "${candidate}") - else() - string(REGEX REPLACE "${gp_regex}" "\\1" raw_item "${candidate}") - endif() - - if(gp_regex_cmp_count GREATER 1) - string(REGEX REPLACE "${gp_regex}" "\\2" raw_compat_version "${candidate}") - string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\1" compat_major_version "${raw_compat_version}") - string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\2" compat_minor_version "${raw_compat_version}") - string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\3" compat_patch_version "${raw_compat_version}") - endif() - - if(gp_regex_cmp_count GREATER 2) - string(REGEX REPLACE "${gp_regex}" "\\3" raw_current_version "${candidate}") - string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\1" current_major_version "${raw_current_version}") - string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\2" current_minor_version "${raw_current_version}") - string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\3" current_patch_version "${raw_current_version}") - endif() - - # Use the raw_item as the list entries returned by this function. Use the - # gp_resolve_item function to resolve it to an actual full path file if - # necessary. - # - set(item "${raw_item}") - - # Add each item unless it is excluded: - # - set(add_item 1) - - if("${item}" STREQUAL "${gp_install_id}") - set(add_item 0) - endif() - - if(add_item AND ${exclude_system}) - set(type "") - gp_resolved_file_type("${target}" "${item}" "${exepath}" "${dirs}" type) - - if("${type}" STREQUAL "system") - set(add_item 0) - endif() - endif() - - if(add_item) - list(LENGTH ${prerequisites_var} list_length_before_append) - gp_append_unique(${prerequisites_var} "${item}") - list(LENGTH ${prerequisites_var} list_length_after_append) - - if(${recurse}) - # If item was really added, this is the first time we have seen it. - # Add it to unseen_prereqs so that we can recursively add *its* - # prerequisites... - # - # But first: resolve its name to an absolute full path name such - # that the analysis tools can simply accept it as input. - # - if(NOT list_length_before_append EQUAL list_length_after_append) - gp_resolve_item("${target}" "${item}" "${exepath}" "${dirs}" resolved_item) - set(unseen_prereqs ${unseen_prereqs} "${resolved_item}") - endif() - endif() - endif() - else() - if(verbose) - message(STATUS "ignoring non-matching line: '${candidate}'") - endif() - endif() - endforeach() - - list(LENGTH ${prerequisites_var} prerequisites_var_length) - if(prerequisites_var_length GREATER 0) - list(SORT ${prerequisites_var}) - endif() - if(${recurse}) - set(more_inputs ${unseen_prereqs}) - foreach(input ${more_inputs}) - get_prerequisites("${input}" ${prerequisites_var} ${exclude_system} ${recurse} "${exepath}" "${dirs}") - endforeach() - endif() - - set(${prerequisites_var} ${${prerequisites_var}} PARENT_SCOPE) -endfunction() - - -function(list_prerequisites target) - if("${ARGV1}" STREQUAL "") - set(all 1) - else() - set(all "${ARGV1}") - endif() - - if("${ARGV2}" STREQUAL "") - set(exclude_system 0) - else() - set(exclude_system "${ARGV2}") - endif() - - if("${ARGV3}" STREQUAL "") - set(verbose 0) - else() - set(verbose "${ARGV3}") - endif() - - set(count 0) - set(count_str "") - set(print_count "${verbose}") - set(print_prerequisite_type "${verbose}") - set(print_target "${verbose}") - set(type_str "") - - get_filename_component(exepath "${target}" PATH) - - set(prereqs "") - get_prerequisites("${target}" prereqs ${exclude_system} ${all} "${exepath}" "") - - if(print_target) - message(STATUS "File '${target}' depends on:") - endif() - - foreach(d ${prereqs}) - math(EXPR count "${count} + 1") - - if(print_count) - set(count_str "${count}. ") - endif() - - if(print_prerequisite_type) - gp_file_type("${target}" "${d}" type) - set(type_str " (${type})") - endif() - - message(STATUS "${count_str}${d}${type_str}") - endforeach() -endfunction() - - -function(list_prerequisites_by_glob glob_arg glob_exp) - message(STATUS "=============================================================================") - message(STATUS "List prerequisites of executables matching ${glob_arg} '${glob_exp}'") - message(STATUS "") - file(${glob_arg} file_list ${glob_exp}) - foreach(f ${file_list}) - is_file_executable("${f}" is_f_executable) - if(is_f_executable) - message(STATUS "=============================================================================") - list_prerequisites("${f}" ${ARGN}) - message(STATUS "") - endif() - endforeach() -endfunction() diff --git a/cmake/UseJava.cmake b/cmake/UseJava.cmake deleted file mode 100644 index 1a5ef107..00000000 --- a/cmake/UseJava.cmake +++ /dev/null @@ -1,881 +0,0 @@ -# - Use Module for Java -# This file provides functions for Java. It is assumed that FindJava.cmake -# has already been loaded. See FindJava.cmake for information on how to -# load Java into your CMake project. -# -# add_jar(TARGET_NAME SRC1 SRC2 .. SRCN RCS1 RCS2 .. RCSN) -# -# This command creates a .jar. It compiles the given source -# files (SRC) and adds the given resource files (RCS) to the jar file. -# If only resource files are given then just a jar file is created. -# -# Additional instructions: -# To add compile flags to the target you can set these flags with -# the following variable: -# -# set(CMAKE_JAVA_COMPILE_FLAGS -nowarn) -# -# To add a path or a jar file to the class path you can do this -# with the CMAKE_JAVA_INCLUDE_PATH variable. -# -# set(CMAKE_JAVA_INCLUDE_PATH /usr/share/java/shibboleet.jar) -# -# To use a different output name for the target you can set it with: -# -# set(CMAKE_JAVA_TARGET_OUTPUT_NAME shibboleet.jar) -# add_jar(foobar foobar.java) -# -# To use a different output directory than CMAKE_CURRENT_BINARY_DIR -# you can set it with: -# -# set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${PROJECT_BINARY_DIR}/bin) -# -# To define an entry point in your jar you can set it with: -# -# set(CMAKE_JAVA_JAR_ENTRY_POINT com/examples/MyProject/Main) -# -# To add a VERSION to the target output name you can set it using -# CMAKE_JAVA_TARGET_VERSION. This will create a jar file with the name -# shibboleet-1.0.0.jar and will create a symlink shibboleet.jar -# pointing to the jar with the version information. -# -# set(CMAKE_JAVA_TARGET_VERSION 1.2.0) -# add_jar(shibboleet shibbotleet.java) -# -# If the target is a JNI library, utilize the following commands to -# create a JNI symbolic link: -# -# set(CMAKE_JNI_TARGET TRUE) -# set(CMAKE_JAVA_TARGET_VERSION 1.2.0) -# add_jar(shibboleet shibbotleet.java) -# install_jar(shibboleet ${LIB_INSTALL_DIR}/shibboleet) -# install_jni_symlink(shibboleet ${JAVA_LIB_INSTALL_DIR}) -# -# If a single target needs to produce more than one jar from its -# java source code, to prevent the accumulation of duplicate class -# files in subsequent jars, set/reset CMAKE_JAR_CLASSES_PREFIX prior -# to calling the add_jar() function: -# -# set(CMAKE_JAR_CLASSES_PREFIX com/redhat/foo) -# add_jar(foo foo.java) -# -# set(CMAKE_JAR_CLASSES_PREFIX com/redhat/bar) -# add_jar(bar bar.java) -# -# Target Properties: -# The add_jar() functions sets some target properties. You can get these -# properties with the -# get_property(TARGET PROPERTY ) -# command. -# -# INSTALL_FILES The files which should be installed. This is used by -# install_jar(). -# JNI_SYMLINK The JNI symlink which should be installed. -# This is used by install_jni_symlink(). -# JAR_FILE The location of the jar file so that you can include -# it. -# CLASS_DIR The directory where the class files can be found. For -# example to use them with javah. -# -# find_jar( -# name | NAMES name1 [name2 ...] -# [PATHS path1 [path2 ... ENV var]] -# [VERSIONS version1 [version2]] -# [DOC "cache documentation string"] -# ) -# -# This command is used to find a full path to the named jar. A cache -# entry named by is created to stor the result of this command. If -# the full path to a jar is found the result is stored in the variable -# and the search will not repeated unless the variable is cleared. If -# nothing is found, the result will be -NOTFOUND, and the search -# will be attempted again next time find_jar is invoked with the same -# variable. -# The name of the full path to a file that is searched for is specified -# by the names listed after NAMES argument. Additional search locations -# can be specified after the PATHS argument. If you require special a -# version of a jar file you can specify it with the VERSIONS argument. -# The argument after DOC will be used for the documentation string in -# the cache. -# -# install_jar(TARGET_NAME DESTINATION) -# -# This command installs the TARGET_NAME files to the given DESTINATION. -# It should be called in the same scope as add_jar() or it will fail. -# -# install_jni_symlink(TARGET_NAME DESTINATION) -# -# This command installs the TARGET_NAME JNI symlinks to the given -# DESTINATION. It should be called in the same scope as add_jar() -# or it will fail. -# -# create_javadoc( -# PACKAGES pkg1 [pkg2 ...] -# [SOURCEPATH ] -# [CLASSPATH ] -# [INSTALLPATH ] -# [DOCTITLE "the documentation title"] -# [WINDOWTITLE "the title of the document"] -# [AUTHOR TRUE|FALSE] -# [USE TRUE|FALSE] -# [VERSION TRUE|FALSE] -# ) -# -# Create java documentation based on files or packages. For more -# details please read the javadoc manpage. -# -# There are two main signatures for create_javadoc. The first -# signature works with package names on a path with source files: -# -# Example: -# create_javadoc(my_example_doc -# PACKAGES com.exmaple.foo com.example.bar -# SOURCEPATH "${CMAKE_CURRENT_SOURCE_DIR}" -# CLASSPATH ${CMAKE_JAVA_INCLUDE_PATH} -# WINDOWTITLE "My example" -# DOCTITLE "

My example

" -# AUTHOR TRUE -# USE TRUE -# VERSION TRUE -# ) -# -# The second signature for create_javadoc works on a given list of -# files. -# -# create_javadoc( -# FILES file1 [file2 ...] -# [CLASSPATH ] -# [INSTALLPATH ] -# [DOCTITLE "the documentation title"] -# [WINDOWTITLE "the title of the document"] -# [AUTHOR TRUE|FALSE] -# [USE TRUE|FALSE] -# [VERSION TRUE|FALSE] -# ) -# -# Example: -# create_javadoc(my_example_doc -# FILES ${example_SRCS} -# CLASSPATH ${CMAKE_JAVA_INCLUDE_PATH} -# WINDOWTITLE "My example" -# DOCTITLE "

My example

" -# AUTHOR TRUE -# USE TRUE -# VERSION TRUE -# ) -# -# Both signatures share most of the options. These options are the -# same as what you can find in the javadoc manpage. Please look at -# the manpage for CLASSPATH, DOCTITLE, WINDOWTITLE, AUTHOR, USE and -# VERSION. -# -# The documentation will be by default installed to -# -# ${CMAKE_INSTALL_PREFIX}/share/javadoc/ -# -# if you don't set the INSTALLPATH. -# - -#============================================================================= -# Copyright 2010-2011 Andreas schneider -# Copyright 2010 Ben Boeckel -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distribute this file outside of CMake, substitute the full -# License text for the above reference.) - -function (__java_copy_file src dest comment) - add_custom_command( - OUTPUT ${dest} - COMMAND cmake -E copy_if_different - ARGS ${src} - ${dest} - DEPENDS ${src} - COMMENT ${comment}) -endfunction (__java_copy_file src dest comment) - -# define helper scripts -set(_JAVA_CLASS_FILELIST_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/UseJavaClassFilelist.cmake) -set(_JAVA_SYMLINK_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/UseJavaSymlinks.cmake) - -function(add_jar _TARGET_NAME) - set(_JAVA_SOURCE_FILES ${ARGN}) - - if (NOT DEFINED CMAKE_JAVA_TARGET_OUTPUT_DIR) - set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}) - endif(NOT DEFINED CMAKE_JAVA_TARGET_OUTPUT_DIR) - - if (CMAKE_JAVA_JAR_ENTRY_POINT) - set(_ENTRY_POINT_OPTION e) - set(_ENTRY_POINT_VALUE ${CMAKE_JAVA_JAR_ENTRY_POINT}) - endif (CMAKE_JAVA_JAR_ENTRY_POINT) - - if (LIBRARY_OUTPUT_PATH) - set(CMAKE_JAVA_LIBRARY_OUTPUT_PATH ${LIBRARY_OUTPUT_PATH}) - else (LIBRARY_OUTPUT_PATH) - set(CMAKE_JAVA_LIBRARY_OUTPUT_PATH ${CMAKE_JAVA_TARGET_OUTPUT_DIR}) - endif (LIBRARY_OUTPUT_PATH) - - set(CMAKE_JAVA_INCLUDE_PATH - ${CMAKE_JAVA_INCLUDE_PATH} - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_JAVA_OBJECT_OUTPUT_PATH} - ${CMAKE_JAVA_LIBRARY_OUTPUT_PATH} - ) - - if (WIN32 AND NOT CYGWIN AND NOT CMAKE_CROSSCOMPILING) - set(CMAKE_JAVA_INCLUDE_FLAG_SEP ";") - else () - set(CMAKE_JAVA_INCLUDE_FLAG_SEP ":") - endif() - - foreach (JAVA_INCLUDE_DIR ${CMAKE_JAVA_INCLUDE_PATH}) - set(CMAKE_JAVA_INCLUDE_PATH_FINAL "${CMAKE_JAVA_INCLUDE_PATH_FINAL}${CMAKE_JAVA_INCLUDE_FLAG_SEP}${JAVA_INCLUDE_DIR}") - endforeach(JAVA_INCLUDE_DIR) - - set(CMAKE_JAVA_CLASS_OUTPUT_PATH "${CMAKE_JAVA_TARGET_OUTPUT_DIR}${CMAKE_FILES_DIRECTORY}/${_TARGET_NAME}.dir") - - set(_JAVA_TARGET_OUTPUT_NAME "${_TARGET_NAME}.jar") - if (CMAKE_JAVA_TARGET_OUTPUT_NAME AND CMAKE_JAVA_TARGET_VERSION) - set(_JAVA_TARGET_OUTPUT_NAME "${CMAKE_JAVA_TARGET_OUTPUT_NAME}-${CMAKE_JAVA_TARGET_VERSION}.jar") - set(_JAVA_TARGET_OUTPUT_LINK "${CMAKE_JAVA_TARGET_OUTPUT_NAME}.jar") - elseif (CMAKE_JAVA_TARGET_VERSION) - set(_JAVA_TARGET_OUTPUT_NAME "${_TARGET_NAME}-${CMAKE_JAVA_TARGET_VERSION}.jar") - set(_JAVA_TARGET_OUTPUT_LINK "${_TARGET_NAME}.jar") - elseif (CMAKE_JAVA_TARGET_OUTPUT_NAME) - set(_JAVA_TARGET_OUTPUT_NAME "${CMAKE_JAVA_TARGET_OUTPUT_NAME}.jar") - endif (CMAKE_JAVA_TARGET_OUTPUT_NAME AND CMAKE_JAVA_TARGET_VERSION) - # reset - set(CMAKE_JAVA_TARGET_OUTPUT_NAME) - - set(_JAVA_CLASS_FILES) - set(_JAVA_COMPILE_FILES) - set(_JAVA_DEPENDS) - set(_JAVA_RESOURCE_FILES) - foreach(_JAVA_SOURCE_FILE ${_JAVA_SOURCE_FILES}) - get_filename_component(_JAVA_EXT ${_JAVA_SOURCE_FILE} EXT) - get_filename_component(_JAVA_FILE ${_JAVA_SOURCE_FILE} NAME_WE) - get_filename_component(_JAVA_PATH ${_JAVA_SOURCE_FILE} PATH) - get_filename_component(_JAVA_FULL ${_JAVA_SOURCE_FILE} ABSOLUTE) - - file(RELATIVE_PATH _JAVA_REL_BINARY_PATH ${CMAKE_JAVA_TARGET_OUTPUT_DIR} ${_JAVA_FULL}) - file(RELATIVE_PATH _JAVA_REL_SOURCE_PATH ${CMAKE_CURRENT_SOURCE_DIR} ${_JAVA_FULL}) - string(LENGTH ${_JAVA_REL_BINARY_PATH} _BIN_LEN) - string(LENGTH ${_JAVA_REL_SOURCE_PATH} _SRC_LEN) - if (${_BIN_LEN} LESS ${_SRC_LEN}) - set(_JAVA_REL_PATH ${_JAVA_REL_BINARY_PATH}) - else (${_BIN_LEN} LESS ${_SRC_LEN}) - set(_JAVA_REL_PATH ${_JAVA_REL_SOURCE_PATH}) - endif (${_BIN_LEN} LESS ${_SRC_LEN}) - get_filename_component(_JAVA_REL_PATH ${_JAVA_REL_PATH} PATH) - - if (_JAVA_EXT MATCHES ".java") - list(APPEND _JAVA_COMPILE_FILES ${_JAVA_SOURCE_FILE}) - set(_JAVA_CLASS_FILE "${CMAKE_JAVA_CLASS_OUTPUT_PATH}/${_JAVA_REL_PATH}/${_JAVA_FILE}.class") - set(_JAVA_CLASS_FILES ${_JAVA_CLASS_FILES} ${_JAVA_CLASS_FILE}) - - elseif (_JAVA_EXT MATCHES ".jar" - OR _JAVA_EXT MATCHES ".war" - OR _JAVA_EXT MATCHES ".ear" - OR _JAVA_EXT MATCHES ".sar") - list(APPEND CMAKE_JAVA_INCLUDE_PATH ${_JAVA_SOURCE_FILE}) - - elseif (_JAVA_EXT STREQUAL "") - list(APPEND CMAKE_JAVA_INCLUDE_PATH ${JAVA_JAR_TARGET_${_JAVA_SOURCE_FILE}} ${JAVA_JAR_TARGET_${_JAVA_SOURCE_FILE}_CLASSPATH}) - list(APPEND _JAVA_DEPENDS ${JAVA_JAR_TARGET_${_JAVA_SOURCE_FILE}}) - - else (_JAVA_EXT MATCHES ".java") - __java_copy_file(${CMAKE_CURRENT_SOURCE_DIR}/${_JAVA_SOURCE_FILE} - ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/${_JAVA_SOURCE_FILE} - "Copying ${_JAVA_SOURCE_FILE} to the build directory") - list(APPEND _JAVA_RESOURCE_FILES ${_JAVA_SOURCE_FILE}) - endif (_JAVA_EXT MATCHES ".java") - endforeach(_JAVA_SOURCE_FILE) - - # create an empty java_class_filelist - if (NOT EXISTS ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist) - file(WRITE ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist "") - endif() - - if (_JAVA_COMPILE_FILES) - # Compile the java files and create a list of class files - add_custom_command( - # NOTE: this command generates an artificial dependency file - OUTPUT ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_compiled_${_TARGET_NAME} - COMMAND ${Java_JAVAC_EXECUTABLE} - ${CMAKE_JAVA_COMPILE_FLAGS} - -classpath "${CMAKE_JAVA_INCLUDE_PATH_FINAL}" - -d ${CMAKE_JAVA_CLASS_OUTPUT_PATH} - ${_JAVA_COMPILE_FILES} - COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_compiled_${_TARGET_NAME} - DEPENDS ${_JAVA_COMPILE_FILES} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMENT "Building Java objects for ${_TARGET_NAME}.jar" - ) - add_custom_command( - OUTPUT ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist - COMMAND ${CMAKE_COMMAND} - -DCMAKE_JAVA_CLASS_OUTPUT_PATH=${CMAKE_JAVA_CLASS_OUTPUT_PATH} - -DCMAKE_JAR_CLASSES_PREFIX="${CMAKE_JAR_CLASSES_PREFIX}" - -P ${_JAVA_CLASS_FILELIST_SCRIPT} - DEPENDS ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_compiled_${_TARGET_NAME} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) - endif (_JAVA_COMPILE_FILES) - - # create the jar file - set(_JAVA_JAR_OUTPUT_PATH - ${CMAKE_JAVA_TARGET_OUTPUT_DIR}/${_JAVA_TARGET_OUTPUT_NAME}) - if (CMAKE_JNI_TARGET) - add_custom_command( - OUTPUT ${_JAVA_JAR_OUTPUT_PATH} - COMMAND ${Java_JAR_EXECUTABLE} - -cf${_ENTRY_POINT_OPTION} ${_JAVA_JAR_OUTPUT_PATH} ${_ENTRY_POINT_VALUE} - ${_JAVA_RESOURCE_FILES} @java_class_filelist - COMMAND ${CMAKE_COMMAND} - -D_JAVA_TARGET_DIR=${CMAKE_JAVA_TARGET_OUTPUT_DIR} - -D_JAVA_TARGET_OUTPUT_NAME=${_JAVA_TARGET_OUTPUT_NAME} - -D_JAVA_TARGET_OUTPUT_LINK=${_JAVA_TARGET_OUTPUT_LINK} - -P ${_JAVA_SYMLINK_SCRIPT} - COMMAND ${CMAKE_COMMAND} - -D_JAVA_TARGET_DIR=${CMAKE_JAVA_TARGET_OUTPUT_DIR} - -D_JAVA_TARGET_OUTPUT_NAME=${_JAVA_JAR_OUTPUT_PATH} - -D_JAVA_TARGET_OUTPUT_LINK=${_JAVA_TARGET_OUTPUT_LINK} - -P ${_JAVA_SYMLINK_SCRIPT} - DEPENDS ${_JAVA_RESOURCE_FILES} ${_JAVA_DEPENDS} ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist - WORKING_DIRECTORY ${CMAKE_JAVA_CLASS_OUTPUT_PATH} - COMMENT "Creating Java archive ${_JAVA_TARGET_OUTPUT_NAME}" - ) - else () - add_custom_command( - OUTPUT ${_JAVA_JAR_OUTPUT_PATH} - COMMAND ${Java_JAR_EXECUTABLE} - -cf${_ENTRY_POINT_OPTION} ${_JAVA_JAR_OUTPUT_PATH} ${_ENTRY_POINT_VALUE} - ${_JAVA_RESOURCE_FILES} @java_class_filelist - COMMAND ${CMAKE_COMMAND} - -D_JAVA_TARGET_DIR=${CMAKE_JAVA_TARGET_OUTPUT_DIR} - -D_JAVA_TARGET_OUTPUT_NAME=${_JAVA_TARGET_OUTPUT_NAME} - -D_JAVA_TARGET_OUTPUT_LINK=${_JAVA_TARGET_OUTPUT_LINK} - -P ${_JAVA_SYMLINK_SCRIPT} - WORKING_DIRECTORY ${CMAKE_JAVA_CLASS_OUTPUT_PATH} - DEPENDS ${_JAVA_RESOURCE_FILES} ${_JAVA_DEPENDS} ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist - COMMENT "Creating Java archive ${_JAVA_TARGET_OUTPUT_NAME}" - ) - endif (CMAKE_JNI_TARGET) - - # Add the target and make sure we have the latest resource files. - add_custom_target(${_TARGET_NAME} ALL DEPENDS ${_JAVA_JAR_OUTPUT_PATH}) - - set_property( - TARGET - ${_TARGET_NAME} - PROPERTY - INSTALL_FILES - ${_JAVA_JAR_OUTPUT_PATH} - ) - - if (_JAVA_TARGET_OUTPUT_LINK) - set_property( - TARGET - ${_TARGET_NAME} - PROPERTY - INSTALL_FILES - ${_JAVA_JAR_OUTPUT_PATH} - ${CMAKE_JAVA_TARGET_OUTPUT_DIR}/${_JAVA_TARGET_OUTPUT_LINK} - ) - - if (CMAKE_JNI_TARGET) - set_property( - TARGET - ${_TARGET_NAME} - PROPERTY - JNI_SYMLINK - ${CMAKE_JAVA_TARGET_OUTPUT_DIR}/${_JAVA_TARGET_OUTPUT_LINK} - ) - endif (CMAKE_JNI_TARGET) - endif (_JAVA_TARGET_OUTPUT_LINK) - - set_property( - TARGET - ${_TARGET_NAME} - PROPERTY - JAR_FILE - ${_JAVA_JAR_OUTPUT_PATH} - ) - - set_property( - TARGET - ${_TARGET_NAME} - PROPERTY - CLASSDIR - ${CMAKE_JAVA_CLASS_OUTPUT_PATH} - ) - -endfunction(add_jar) - -function(INSTALL_JAR _TARGET_NAME _DESTINATION) - get_property(__FILES - TARGET - ${_TARGET_NAME} - PROPERTY - INSTALL_FILES - ) - - if (__FILES) - install( - FILES - ${__FILES} - DESTINATION - ${_DESTINATION} - ) - else (__FILES) - message(SEND_ERROR "The target ${_TARGET_NAME} is not known in this scope.") - endif (__FILES) -endfunction(INSTALL_JAR _TARGET_NAME _DESTINATION) - -function(INSTALL_JNI_SYMLINK _TARGET_NAME _DESTINATION) - get_property(__SYMLINK - TARGET - ${_TARGET_NAME} - PROPERTY - JNI_SYMLINK - ) - - if (__SYMLINK) - install( - FILES - ${__SYMLINK} - DESTINATION - ${_DESTINATION} - ) - else (__SYMLINK) - message(SEND_ERROR "The target ${_TARGET_NAME} is not known in this scope.") - endif (__SYMLINK) -endfunction(INSTALL_JNI_SYMLINK _TARGET_NAME _DESTINATION) - -function (find_jar VARIABLE) - set(_jar_names) - set(_jar_files) - set(_jar_versions) - set(_jar_paths - /usr/share/java/ - /usr/local/share/java/ - ${Java_JAR_PATHS}) - set(_jar_doc "NOTSET") - - set(_state "name") - - foreach (arg ${ARGN}) - if (${_state} STREQUAL "name") - if (${arg} STREQUAL "VERSIONS") - set(_state "versions") - elseif (${arg} STREQUAL "NAMES") - set(_state "names") - elseif (${arg} STREQUAL "PATHS") - set(_state "paths") - elseif (${arg} STREQUAL "DOC") - set(_state "doc") - else (${arg} STREQUAL "NAMES") - set(_jar_names ${arg}) - if (_jar_doc STREQUAL "NOTSET") - set(_jar_doc "Finding ${arg} jar") - endif (_jar_doc STREQUAL "NOTSET") - endif (${arg} STREQUAL "VERSIONS") - elseif (${_state} STREQUAL "versions") - if (${arg} STREQUAL "NAMES") - set(_state "names") - elseif (${arg} STREQUAL "PATHS") - set(_state "paths") - elseif (${arg} STREQUAL "DOC") - set(_state "doc") - else (${arg} STREQUAL "NAMES") - set(_jar_versions ${_jar_versions} ${arg}) - endif (${arg} STREQUAL "NAMES") - elseif (${_state} STREQUAL "names") - if (${arg} STREQUAL "VERSIONS") - set(_state "versions") - elseif (${arg} STREQUAL "PATHS") - set(_state "paths") - elseif (${arg} STREQUAL "DOC") - set(_state "doc") - else (${arg} STREQUAL "VERSIONS") - set(_jar_names ${_jar_names} ${arg}) - if (_jar_doc STREQUAL "NOTSET") - set(_jar_doc "Finding ${arg} jar") - endif (_jar_doc STREQUAL "NOTSET") - endif (${arg} STREQUAL "VERSIONS") - elseif (${_state} STREQUAL "paths") - if (${arg} STREQUAL "VERSIONS") - set(_state "versions") - elseif (${arg} STREQUAL "NAMES") - set(_state "names") - elseif (${arg} STREQUAL "DOC") - set(_state "doc") - else (${arg} STREQUAL "VERSIONS") - set(_jar_paths ${_jar_paths} ${arg}) - endif (${arg} STREQUAL "VERSIONS") - elseif (${_state} STREQUAL "doc") - if (${arg} STREQUAL "VERSIONS") - set(_state "versions") - elseif (${arg} STREQUAL "NAMES") - set(_state "names") - elseif (${arg} STREQUAL "PATHS") - set(_state "paths") - else (${arg} STREQUAL "VERSIONS") - set(_jar_doc ${arg}) - endif (${arg} STREQUAL "VERSIONS") - endif (${_state} STREQUAL "name") - endforeach (arg ${ARGN}) - - if (NOT _jar_names) - message(FATAL_ERROR "find_jar: No name to search for given") - endif (NOT _jar_names) - - foreach (jar_name ${_jar_names}) - foreach (version ${_jar_versions}) - set(_jar_files ${_jar_files} ${jar_name}-${version}.jar) - endforeach (version ${_jar_versions}) - set(_jar_files ${_jar_files} ${jar_name}.jar) - endforeach (jar_name ${_jar_names}) - - find_file(${VARIABLE} - NAMES ${_jar_files} - PATHS ${_jar_paths} - DOC ${_jar_doc} - NO_DEFAULT_PATH) -endfunction (find_jar VARIABLE) - -function(create_javadoc _target) - set(_javadoc_packages) - set(_javadoc_files) - set(_javadoc_sourcepath) - set(_javadoc_classpath) - set(_javadoc_installpath "${CMAKE_INSTALL_PREFIX}/share/javadoc") - set(_javadoc_doctitle) - set(_javadoc_windowtitle) - set(_javadoc_author FALSE) - set(_javadoc_version FALSE) - set(_javadoc_use FALSE) - - set(_state "package") - - foreach (arg ${ARGN}) - if (${_state} STREQUAL "package") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - set(_javadoc_packages ${arg}) - set(_state "packages") - endif () - elseif (${_state} STREQUAL "packages") - if (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - list(APPEND _javadoc_packages ${arg}) - endif () - elseif (${_state} STREQUAL "files") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - list(APPEND _javadoc_files ${arg}) - endif () - elseif (${_state} STREQUAL "sourcepath") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - list(APPEND _javadoc_sourcepath ${arg}) - endif () - elseif (${_state} STREQUAL "classpath") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - list(APPEND _javadoc_classpath ${arg}) - endif () - elseif (${_state} STREQUAL "installpath") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - set(_javadoc_installpath ${arg}) - endif () - elseif (${_state} STREQUAL "doctitle") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - set(_javadoc_doctitle ${arg}) - endif () - elseif (${_state} STREQUAL "windowtitle") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - set(_javadoc_windowtitle ${arg}) - endif () - elseif (${_state} STREQUAL "author") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - set(_javadoc_author ${arg}) - endif () - elseif (${_state} STREQUAL "use") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - set(_javadoc_use ${arg}) - endif () - elseif (${_state} STREQUAL "version") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - set(_javadoc_version ${arg}) - endif () - endif (${_state} STREQUAL "package") - endforeach (arg ${ARGN}) - - set(_javadoc_builddir ${CMAKE_CURRENT_BINARY_DIR}/javadoc/${_target}) - set(_javadoc_options -d ${_javadoc_builddir}) - - if (_javadoc_sourcepath) - set(_start TRUE) - foreach(_path ${_javadoc_sourcepath}) - if (_start) - set(_sourcepath ${_path}) - set(_start FALSE) - else (_start) - set(_sourcepath ${_sourcepath}:${_path}) - endif (_start) - endforeach(_path ${_javadoc_sourcepath}) - set(_javadoc_options ${_javadoc_options} -sourcepath ${_sourcepath}) - endif (_javadoc_sourcepath) - - if (_javadoc_classpath) - set(_start TRUE) - foreach(_path ${_javadoc_classpath}) - if (_start) - set(_classpath ${_path}) - set(_start FALSE) - else (_start) - set(_classpath ${_classpath}:${_path}) - endif (_start) - endforeach(_path ${_javadoc_classpath}) - set(_javadoc_options ${_javadoc_options} -classpath "${_classpath}") - endif (_javadoc_classpath) - - if (_javadoc_doctitle) - set(_javadoc_options ${_javadoc_options} -doctitle '${_javadoc_doctitle}') - endif (_javadoc_doctitle) - - if (_javadoc_windowtitle) - set(_javadoc_options ${_javadoc_options} -windowtitle '${_javadoc_windowtitle}') - endif (_javadoc_windowtitle) - - if (_javadoc_author) - set(_javadoc_options ${_javadoc_options} -author) - endif (_javadoc_author) - - if (_javadoc_use) - set(_javadoc_options ${_javadoc_options} -use) - endif (_javadoc_use) - - if (_javadoc_version) - set(_javadoc_options ${_javadoc_options} -version) - endif (_javadoc_version) - - add_custom_target(${_target}_javadoc ALL - COMMAND ${Java_JAVADOC_EXECUTABLE} ${_javadoc_options} - ${_javadoc_files} - ${_javadoc_packages} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) - - install( - DIRECTORY ${_javadoc_builddir} - DESTINATION ${_javadoc_installpath} - ) -endfunction(create_javadoc) diff --git a/cmake/UseJavaClassFilelist.cmake b/cmake/UseJavaClassFilelist.cmake deleted file mode 100644 index c842bf71..00000000 --- a/cmake/UseJavaClassFilelist.cmake +++ /dev/null @@ -1,52 +0,0 @@ -# -# This script create a list of compiled Java class files to be added to a -# jar file. This avoids including cmake files which get created in the -# binary directory. -# - -#============================================================================= -# Copyright 2010-2011 Andreas schneider -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distribute this file outside of CMake, substitute the full -# License text for the above reference.) - -if (CMAKE_JAVA_CLASS_OUTPUT_PATH) - if (EXISTS "${CMAKE_JAVA_CLASS_OUTPUT_PATH}") - - set(_JAVA_GLOBBED_FILES) - if (CMAKE_JAR_CLASSES_PREFIX) - foreach(JAR_CLASS_PREFIX ${CMAKE_JAR_CLASSES_PREFIX}) - message(STATUS "JAR_CLASS_PREFIX: ${JAR_CLASS_PREFIX}") - - file(GLOB_RECURSE _JAVA_GLOBBED_TMP_FILES "${CMAKE_JAVA_CLASS_OUTPUT_PATH}/${JAR_CLASS_PREFIX}/*.class") - if (_JAVA_GLOBBED_TMP_FILES) - list(APPEND _JAVA_GLOBBED_FILES ${_JAVA_GLOBBED_TMP_FILES}) - endif (_JAVA_GLOBBED_TMP_FILES) - endforeach(JAR_CLASS_PREFIX ${CMAKE_JAR_CLASSES_PREFIX}) - else() - file(GLOB_RECURSE _JAVA_GLOBBED_FILES "${CMAKE_JAVA_CLASS_OUTPUT_PATH}/*.class") - endif (CMAKE_JAR_CLASSES_PREFIX) - - set(_JAVA_CLASS_FILES) - # file(GLOB_RECURSE foo RELATIVE) is broken so we need this. - foreach(_JAVA_GLOBBED_FILE ${_JAVA_GLOBBED_FILES}) - file(RELATIVE_PATH _JAVA_CLASS_FILE ${CMAKE_JAVA_CLASS_OUTPUT_PATH} ${_JAVA_GLOBBED_FILE}) - set(_JAVA_CLASS_FILES ${_JAVA_CLASS_FILES}${_JAVA_CLASS_FILE}\n) - endforeach(_JAVA_GLOBBED_FILE ${_JAVA_GLOBBED_FILES}) - - # write to file - file(WRITE ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist ${_JAVA_CLASS_FILES}) - - else (EXISTS "${CMAKE_JAVA_CLASS_OUTPUT_PATH}") - message(SEND_ERROR "FATAL: Java class output path doesn't exist") - endif (EXISTS "${CMAKE_JAVA_CLASS_OUTPUT_PATH}") -else (CMAKE_JAVA_CLASS_OUTPUT_PATH) - message(SEND_ERROR "FATAL: Can't find CMAKE_JAVA_CLASS_OUTPUT_PATH") -endif (CMAKE_JAVA_CLASS_OUTPUT_PATH) diff --git a/cmake/UseJavaSymlinks.cmake b/cmake/UseJavaSymlinks.cmake deleted file mode 100644 index c66ee1ea..00000000 --- a/cmake/UseJavaSymlinks.cmake +++ /dev/null @@ -1,32 +0,0 @@ -# -# Helper script for UseJava.cmake -# - -#============================================================================= -# Copyright 2010-2011 Andreas schneider -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distribute this file outside of CMake, substitute the full -# License text for the above reference.) - -if (UNIX AND _JAVA_TARGET_OUTPUT_LINK) - if (_JAVA_TARGET_OUTPUT_NAME) - find_program(LN_EXECUTABLE - NAMES - ln - ) - - execute_process( - COMMAND ${LN_EXECUTABLE} -sf "${_JAVA_TARGET_OUTPUT_NAME}" "${_JAVA_TARGET_OUTPUT_LINK}" - WORKING_DIRECTORY ${_JAVA_TARGET_DIR} - ) - else (_JAVA_TARGET_OUTPUT_NAME) - message(SEND_ERROR "FATAL: Can't find _JAVA_TARGET_OUTPUT_NAME") - endif (_JAVA_TARGET_OUTPUT_NAME) -endif (UNIX AND _JAVA_TARGET_OUTPUT_LINK) From 8de63b60b1a9d0ba16f5d45f3198c13637151749 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Mon, 2 May 2022 22:36:55 +0100 Subject: [PATCH 330/605] Refactor some parts of NewLaunch (part 2) --- libraries/launcher/CMakeLists.txt | 13 +- .../launcher/net/minecraft/Launcher.java | 92 +++---- .../launcher/org/multimc/EntryPoint.java | 55 ++-- libraries/launcher/org/multimc/Launcher.java | 7 +- .../launcher/org/multimc/LauncherFactory.java | 34 +++ .../launcher/org/multimc/LegacyFrame.java | 176 ------------ libraries/launcher/org/multimc/Utils.java | 86 ------ .../org/multimc/applet/LegacyFrame.java | 167 ++++++++++++ .../ParameterNotFoundException.java} | 10 +- .../{ => exception}/ParseException.java | 12 +- .../org/multimc/impl/OneSixLauncher.java | 183 +++++++++++++ .../org/multimc/onesix/OneSixLauncher.java | 256 ------------------ .../org/multimc/{ => utils}/ParamBucket.java | 36 +-- .../launcher/org/multimc/utils/Utils.java | 49 ++++ 14 files changed, 542 insertions(+), 634 deletions(-) create mode 100644 libraries/launcher/org/multimc/LauncherFactory.java delete mode 100644 libraries/launcher/org/multimc/LegacyFrame.java delete mode 100644 libraries/launcher/org/multimc/Utils.java create mode 100644 libraries/launcher/org/multimc/applet/LegacyFrame.java rename libraries/launcher/org/multimc/{NotFoundException.java => exception/ParameterNotFoundException.java} (73%) rename libraries/launcher/org/multimc/{ => exception}/ParseException.java (81%) create mode 100644 libraries/launcher/org/multimc/impl/OneSixLauncher.java delete mode 100644 libraries/launcher/org/multimc/onesix/OneSixLauncher.java rename libraries/launcher/org/multimc/{ => utils}/ParamBucket.java (68%) create mode 100644 libraries/launcher/org/multimc/utils/Utils.java diff --git a/libraries/launcher/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index 0eccae8b..e0149482 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -9,12 +9,13 @@ set(CMAKE_JAVA_COMPILE_FLAGS -target 8 -source 8 -Xlint:deprecation -Xlint:unche set(SRC org/multimc/EntryPoint.java org/multimc/Launcher.java - org/multimc/LegacyFrame.java - org/multimc/NotFoundException.java - org/multimc/ParamBucket.java - org/multimc/ParseException.java - org/multimc/Utils.java - org/multimc/onesix/OneSixLauncher.java + org/multimc/LauncherFactory.java + org/multimc/impl/OneSixLauncher.java + org/multimc/applet/LegacyFrame.java + org/multimc/exception/ParameterNotFoundException.java + org/multimc/exception/ParseException.java + org/multimc/utils/ParamBucket.java + org/multimc/utils/Utils.java net/minecraft/Launcher.java ) add_jar(NewLaunch ${SRC}) diff --git a/libraries/launcher/net/minecraft/Launcher.java b/libraries/launcher/net/minecraft/Launcher.java index b6b0a574..04201047 100644 --- a/libraries/launcher/net/minecraft/Launcher.java +++ b/libraries/launcher/net/minecraft/Launcher.java @@ -16,29 +16,28 @@ package net.minecraft; -import java.util.TreeMap; -import java.util.Map; -import java.net.URL; -import java.awt.Dimension; -import java.awt.BorderLayout; -import java.awt.Graphics; import java.applet.Applet; import java.applet.AppletStub; +import java.awt.*; import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; +import java.util.TreeMap; + +public class Launcher extends Applet implements AppletStub { + + private final Map params = new TreeMap<>(); + + private boolean active = false; -public class Launcher extends Applet implements AppletStub -{ private Applet wrappedApplet; private URL documentBase; - private boolean active = false; - private final Map params; - - public Launcher(Applet applet, URL documentBase) - { - params = new TreeMap(); + public Launcher(Applet applet, URL documentBase) { this.setLayout(new BorderLayout()); + this.add(applet, "Center"); + this.wrappedApplet = applet; this.documentBase = documentBase; } @@ -48,8 +47,7 @@ public class Launcher extends Applet implements AppletStub params.put(name, value); } - public void replace(Applet applet) - { + public void replace(Applet applet) { this.wrappedApplet = applet; applet.setStub(this); @@ -65,67 +63,60 @@ public class Launcher extends Applet implements AppletStub } @Override - public String getParameter(String name) - { + public String getParameter(String name) { String param = params.get(name); + if (param != null) return param; - try - { + + try { return super.getParameter(name); - } catch (Exception ignore){} + } catch (Exception ignore) {} + return null; } @Override - public boolean isActive() - { + public boolean isActive() { return active; } @Override - public void appletResize(int width, int height) - { + public void appletResize(int width, int height) { wrappedApplet.resize(width, height); } @Override - public void resize(int width, int height) - { + public void resize(int width, int height) { wrappedApplet.resize(width, height); } @Override - public void resize(Dimension d) - { + public void resize(Dimension d) { wrappedApplet.resize(d); } @Override - public void init() - { + public void init() { if (wrappedApplet != null) - { wrappedApplet.init(); - } } @Override - public void start() - { + public void start() { wrappedApplet.start(); + active = true; } @Override - public void stop() - { + public void stop() { wrappedApplet.stop(); + active = false; } - public void destroy() - { + public void destroy() { wrappedApplet.destroy(); } @@ -136,34 +127,35 @@ public class Launcher extends Applet implements AppletStub } catch (MalformedURLException e) { e.printStackTrace(); } + return null; } @Override - public URL getDocumentBase() - { + public URL getDocumentBase() { try { // Special case only for Classic versions if (wrappedApplet.getClass().getCanonicalName().startsWith("com.mojang")) { return new URL("http", "www.minecraft.net", 80, "/game/", null); } + return new URL("http://www.minecraft.net/game/"); } catch (MalformedURLException e) { e.printStackTrace(); } + return null; } @Override - public void setVisible(boolean b) - { + public void setVisible(boolean b) { super.setVisible(b); + wrappedApplet.setVisible(b); } - public void update(Graphics paramGraphics) - { - } - public void paint(Graphics paramGraphics) - { - } -} \ No newline at end of file + + public void update(Graphics paramGraphics) {} + + public void paint(Graphics paramGraphics) {} + +} diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index b626d095..be06d1b4 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -14,7 +14,8 @@ package org.multimc;/* * limitations under the License. */ -import org.multimc.onesix.OneSixLauncher; +import org.multimc.exception.ParseException; +import org.multimc.utils.ParamBucket; import java.io.BufferedReader; import java.io.IOException; @@ -23,31 +24,27 @@ import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; -public class EntryPoint -{ +public final class EntryPoint { private static final Logger LOGGER = Logger.getLogger("EntryPoint"); private final ParamBucket params = new ParamBucket(); - private org.multimc.Launcher launcher; + private String launcherType; - public static void main(String[] args) - { + public static void main(String[] args) { EntryPoint listener = new EntryPoint(); int retCode = listener.listen(); - if (retCode != 0) - { + if (retCode != 0) { LOGGER.info("Exiting with " + retCode); System.exit(retCode); } } - private Action parseLine(String inData) throws ParseException - { + private Action parseLine(String inData) throws ParseException { String[] tokens = inData.split("\\s+", 2); if (tokens.length == 0) @@ -66,15 +63,9 @@ public class EntryPoint if (tokens.length != 2) throw new ParseException("Expected 2 tokens, got " + tokens.length); - if (tokens[1].equals("onesix")) { - launcher = new OneSixLauncher(); + launcherType = tokens[1]; - LOGGER.info("Using onesix launcher."); - - return Action.Proceed; - } else { - throw new ParseException("Invalid launcher type: " + tokens[1]); - } + return Action.Proceed; } default: { @@ -88,8 +79,7 @@ public class EntryPoint } } - public int listen() - { + public int listen() { Action action = Action.Proceed; try (BufferedReader reader = new BufferedReader(new InputStreamReader( @@ -112,16 +102,31 @@ public class EntryPoint } // Main loop - if (action == Action.Abort) - { + if (action == Action.Abort) { LOGGER.info("Launch aborted by the launcher."); return 1; } - if (launcher != null) - { - return launcher.launch(params); + if (launcherType != null) { + try { + Launcher launcher = + LauncherFactory + .getInstance() + .createLauncher(launcherType, params); + + launcher.launch(); + + return 0; + } catch (IllegalArgumentException e) { + LOGGER.log(Level.SEVERE, "Wrong argument.", e); + + return 1; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Exception caught from launcher.", e); + + return 1; + } } LOGGER.log(Level.SEVERE, "No valid launcher implementation specified."); diff --git a/libraries/launcher/org/multimc/Launcher.java b/libraries/launcher/org/multimc/Launcher.java index c5e8fbc1..bc0b525e 100644 --- a/libraries/launcher/org/multimc/Launcher.java +++ b/libraries/launcher/org/multimc/Launcher.java @@ -16,7 +16,8 @@ package org.multimc; -public interface Launcher -{ - int launch(ParamBucket params); +public interface Launcher { + + void launch() throws Exception; + } diff --git a/libraries/launcher/org/multimc/LauncherFactory.java b/libraries/launcher/org/multimc/LauncherFactory.java new file mode 100644 index 00000000..b5d0dd5b --- /dev/null +++ b/libraries/launcher/org/multimc/LauncherFactory.java @@ -0,0 +1,34 @@ +package org.multimc; + +import org.multimc.impl.OneSixLauncher; +import org.multimc.utils.ParamBucket; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +public final class LauncherFactory { + + private static final LauncherFactory INSTANCE = new LauncherFactory(); + + private final Map> launcherRegistry = new HashMap<>(); + + private LauncherFactory() { + launcherRegistry.put("onesix", OneSixLauncher::new); + } + + public Launcher createLauncher(String name, ParamBucket parameters) { + Function launcherCreator = + launcherRegistry.get(name); + + if (launcherCreator == null) + throw new IllegalArgumentException("Invalid launcher type: " + name); + + return launcherCreator.apply(parameters); + } + + public static LauncherFactory getInstance() { + return INSTANCE; + } + +} diff --git a/libraries/launcher/org/multimc/LegacyFrame.java b/libraries/launcher/org/multimc/LegacyFrame.java deleted file mode 100644 index 985a10e6..00000000 --- a/libraries/launcher/org/multimc/LegacyFrame.java +++ /dev/null @@ -1,176 +0,0 @@ -package org.multimc;/* - * Copyright 2012-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. - */ - -import net.minecraft.Launcher; - -import javax.imageio.ImageIO; -import java.applet.Applet; -import java.awt.*; -import java.awt.event.WindowEvent; -import java.awt.event.WindowListener; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Scanner; - -public class LegacyFrame extends Frame implements WindowListener -{ - private Launcher appletWrap = null; - public LegacyFrame(String title) - { - super ( title ); - BufferedImage image; - try { - image = ImageIO.read ( new File ( "icon.png" ) ); - setIconImage ( image ); - } catch ( IOException e ) { - e.printStackTrace(); - } - this.addWindowListener ( this ); - } - - public void start ( - Applet mcApplet, - String user, - String session, - int winSizeW, - int winSizeH, - boolean maximize, - String serverAddress, - String serverPort - ) - { - try { - appletWrap = new Launcher( mcApplet, new URL ( "http://www.minecraft.net/game" ) ); - } catch ( MalformedURLException ignored ) {} - - // Implements support for launching in to multiplayer on classic servers using a mpticket - // file generated by an external program and stored in the instance's root folder. - File mpticketFile = null; - Scanner fileReader = null; - try { - mpticketFile = new File(System.getProperty("user.dir") + "/../mpticket").getCanonicalFile(); - fileReader = new Scanner(new FileInputStream(mpticketFile), "ascii"); - String[] mpticketParams = new String[3]; - - for(int i=0;i<3;i++) { - if(fileReader.hasNextLine()) { - mpticketParams[i] = fileReader.nextLine(); - } else { - throw new IllegalArgumentException(); - } - } - - // Assumes parameters are valid and in the correct order - appletWrap.setParameter("server", mpticketParams[0]); - appletWrap.setParameter("port", mpticketParams[1]); - appletWrap.setParameter("mppass", mpticketParams[2]); - - fileReader.close(); - mpticketFile.delete(); - } - catch (FileNotFoundException e) {} - catch (IllegalArgumentException e) { - - fileReader.close(); - File mpticketFileCorrupt = new File(System.getProperty("user.dir") + "/../mpticket.corrupt"); - if(mpticketFileCorrupt.exists()) { - mpticketFileCorrupt.delete(); - } - mpticketFile.renameTo(mpticketFileCorrupt); - - System.err.println("Malformed mpticket file, missing argument."); - e.printStackTrace(System.err); - System.exit(-1); - } - catch (Exception e) { - e.printStackTrace(System.err); - System.exit(-1); - } - - if (serverAddress != null) - { - appletWrap.setParameter("server", serverAddress); - appletWrap.setParameter("port", serverPort); - } - - appletWrap.setParameter ( "username", user ); - appletWrap.setParameter ( "sessionid", session ); - appletWrap.setParameter ( "stand-alone", "true" ); // Show the quit button. - appletWrap.setParameter ( "haspaid", "true" ); // Some old versions need this for world saves to work. - appletWrap.setParameter ( "demo", "false" ); - appletWrap.setParameter ( "fullscreen", "false" ); - mcApplet.setStub(appletWrap); - this.add ( appletWrap ); - appletWrap.setPreferredSize ( new Dimension (winSizeW, winSizeH) ); - this.pack(); - this.setLocationRelativeTo ( null ); - this.setResizable ( true ); - if ( maximize ) { - this.setExtendedState ( MAXIMIZED_BOTH ); - } - validate(); - appletWrap.init(); - appletWrap.start(); - setVisible ( true ); - } - - @Override - public void windowActivated ( WindowEvent e ) {} - - @Override - public void windowClosed ( WindowEvent e ) {} - - @Override - public void windowClosing ( WindowEvent e ) - { - new Thread() { - public void run() { - try { - Thread.sleep ( 30000L ); - } catch ( InterruptedException localInterruptedException ) { - localInterruptedException.printStackTrace(); - } - System.out.println ( "FORCING EXIT!" ); - System.exit ( 0 ); - } - } - .start(); - - if ( appletWrap != null ) { - appletWrap.stop(); - appletWrap.destroy(); - } - // old minecraft versions can hang without this >_< - System.exit ( 0 ); - } - - @Override - public void windowDeactivated ( WindowEvent e ) {} - - @Override - public void windowDeiconified ( WindowEvent e ) {} - - @Override - public void windowIconified ( WindowEvent e ) {} - - @Override - public void windowOpened ( WindowEvent e ) {} -} diff --git a/libraries/launcher/org/multimc/Utils.java b/libraries/launcher/org/multimc/Utils.java deleted file mode 100644 index e48029c2..00000000 --- a/libraries/launcher/org/multimc/Utils.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2012-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. - */ - -package org.multimc; - -import java.io.File; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.List; - -public class Utils -{ - /** - * Combine two parts of a path. - * - * @param path1 - * @param path2 - * @return the paths, combined - */ - public static String combine(String path1, String path2) - { - File file1 = new File(path1); - File file2 = new File(file1, path2); - return file2.getPath(); - } - - /** - * Join a list of strings into a string using a separator! - * - * @param strings the string list to join - * @param separator the glue - * @return the result. - */ - public static String join(List strings, String separator) - { - StringBuilder sb = new StringBuilder(); - String sep = ""; - for (String s : strings) - { - sb.append(sep).append(s); - sep = separator; - } - return sb.toString(); - } - - /** - * Finds a field that looks like a Minecraft base folder in a supplied class - * - * @param mc the class to scan - */ - public static Field getMCPathField(Class mc) - { - Field[] fields = mc.getDeclaredFields(); - - for (Field f : fields) - { - if (f.getType() != File.class) - { - // Has to be File - continue; - } - if (f.getModifiers() != (Modifier.PRIVATE + Modifier.STATIC)) - { - // And Private Static. - continue; - } - return f; - } - return null; - } - -} - diff --git a/libraries/launcher/org/multimc/applet/LegacyFrame.java b/libraries/launcher/org/multimc/applet/LegacyFrame.java new file mode 100644 index 00000000..a5e6c170 --- /dev/null +++ b/libraries/launcher/org/multimc/applet/LegacyFrame.java @@ -0,0 +1,167 @@ +package org.multimc.applet;/* + * Copyright 2012-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. + */ + +import net.minecraft.Launcher; + +import javax.imageio.ImageIO; +import java.applet.Applet; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class LegacyFrame extends Frame { + + private static final Logger LOGGER = Logger.getLogger("LegacyFrame"); + + private Launcher appletWrap; + + public LegacyFrame(String title) { + super(title); + + try { + setIconImage(ImageIO.read(new File("icon.png"))); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Unable to read Minecraft icon!", e); + } + + this.addWindowListener(new ForceExitHandler()); + } + + public void start ( + Applet mcApplet, + String user, + String session, + int winSizeW, + int winSizeH, + boolean maximize, + String serverAddress, + String serverPort + ) { + try { + appletWrap = new Launcher(mcApplet, new URL("http://www.minecraft.net/game")); + } catch (MalformedURLException ignored) {} + + // Implements support for launching in to multiplayer on classic servers using a mpticket + // file generated by an external program and stored in the instance's root folder. + Path mpticketFile = Paths.get(System.getProperty("user.dir") + "/../mpticket"); + Path mpticketFileCorrupt = Paths.get(System.getProperty("user.dir") + "/../mpticket.corrupt"); + + if (Files.exists(mpticketFile)) { + try (Scanner fileScanner = new Scanner( + Files.newInputStream(mpticketFile), + StandardCharsets.US_ASCII.name() + )) { + String[] mpticketParams = new String[3]; + + for (int i = 0; i < mpticketParams.length; i++) { + if (fileScanner.hasNextLine()) { + mpticketParams[i] = fileScanner.nextLine(); + } else { + Files.move( + mpticketFile, + mpticketFileCorrupt, + StandardCopyOption.REPLACE_EXISTING + ); + + throw new IllegalArgumentException("Mpticket file is corrupted!"); + } + } + + Files.delete(mpticketFile); + + // Assumes parameters are valid and in the correct order + appletWrap.setParameter("server", mpticketParams[0]); + appletWrap.setParameter("port", mpticketParams[1]); + appletWrap.setParameter("mppass", mpticketParams[2]); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Unable to read mpticket file!", e); + } + } + + if (serverAddress != null) { + appletWrap.setParameter("server", serverAddress); + appletWrap.setParameter("port", serverPort); + } + + appletWrap.setParameter("username", user); + appletWrap.setParameter("sessionid", session); + appletWrap.setParameter("stand-alone", "true"); // Show the quit button. + appletWrap.setParameter("haspaid", "true"); // Some old versions need this for world saves to work. + appletWrap.setParameter("demo", "false"); + appletWrap.setParameter("fullscreen", "false"); + + mcApplet.setStub(appletWrap); + + add(appletWrap); + + appletWrap.setPreferredSize(new Dimension(winSizeW, winSizeH)); + + pack(); + + setLocationRelativeTo(null); + setResizable(true); + + if (maximize) + this.setExtendedState(MAXIMIZED_BOTH); + + validate(); + + appletWrap.init(); + appletWrap.start(); + + setVisible(true); + } + + private final class ForceExitHandler extends WindowAdapter { + + @Override + public void windowClosing(WindowEvent e) { + new Thread(() -> { + try { + Thread.sleep(30000L); + } catch (InterruptedException localInterruptedException) { + localInterruptedException.printStackTrace(); + } + + LOGGER.info("Forcing exit!"); + + System.exit(0); + }).start(); + + if (appletWrap != null) { + appletWrap.stop(); + appletWrap.destroy(); + } + + // old minecraft versions can hang without this >_< + System.exit(0); + } + + } + +} diff --git a/libraries/launcher/org/multimc/NotFoundException.java b/libraries/launcher/org/multimc/exception/ParameterNotFoundException.java similarity index 73% rename from libraries/launcher/org/multimc/NotFoundException.java rename to libraries/launcher/org/multimc/exception/ParameterNotFoundException.java index ba12951d..9edbb826 100644 --- a/libraries/launcher/org/multimc/NotFoundException.java +++ b/libraries/launcher/org/multimc/exception/ParameterNotFoundException.java @@ -14,8 +14,12 @@ * limitations under the License. */ -package org.multimc; +package org.multimc.exception; + +public final class ParameterNotFoundException extends IllegalArgumentException { + + public ParameterNotFoundException(String key) { + super("Unknown parameter name: " + key); + } -public class NotFoundException extends Exception -{ } diff --git a/libraries/launcher/org/multimc/ParseException.java b/libraries/launcher/org/multimc/exception/ParseException.java similarity index 81% rename from libraries/launcher/org/multimc/ParseException.java rename to libraries/launcher/org/multimc/exception/ParseException.java index 7ea44c1f..c9a4c856 100644 --- a/libraries/launcher/org/multimc/ParseException.java +++ b/libraries/launcher/org/multimc/exception/ParseException.java @@ -14,12 +14,16 @@ * limitations under the License. */ -package org.multimc; +package org.multimc.exception; + +public final class ParseException extends IllegalArgumentException { + + public ParseException() { + super(); + } -public class ParseException extends java.lang.Exception -{ - public ParseException() { super(); } public ParseException(String message) { super(message); } + } diff --git a/libraries/launcher/org/multimc/impl/OneSixLauncher.java b/libraries/launcher/org/multimc/impl/OneSixLauncher.java new file mode 100644 index 00000000..d2596a69 --- /dev/null +++ b/libraries/launcher/org/multimc/impl/OneSixLauncher.java @@ -0,0 +1,183 @@ +/* Copyright 2012-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. + */ + +package org.multimc.impl; + +import org.multimc.Launcher; +import org.multimc.applet.LegacyFrame; +import org.multimc.utils.ParamBucket; +import org.multimc.utils.Utils; + +import java.applet.Applet; +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class OneSixLauncher implements Launcher { + + private static final int DEFAULT_WINDOW_WIDTH = 854; + private static final int DEFAULT_WINDOW_HEIGHT = 480; + + private static final Logger LOGGER = Logger.getLogger("OneSixLauncher"); + + // parameters, separated from ParamBucket + private final List mcParams; + private final List traits; + private final String appletClass; + private final String mainClass; + private final String userName, sessionId; + private final String windowTitle; + + // secondary parameters + private final int winSizeW; + private final int winSizeH; + private final boolean maximize; + private final String cwd; + + private final String serverAddress; + private final String serverPort; + + private final ClassLoader classLoader; + + public OneSixLauncher(ParamBucket params) { + classLoader = ClassLoader.getSystemClassLoader(); + + mcParams = params.allSafe("param", Collections.emptyList()); + mainClass = params.firstSafe("mainClass", "net.minecraft.client.Minecraft"); + appletClass = params.firstSafe("appletClass", "net.minecraft.client.MinecraftApplet"); + traits = params.allSafe("traits", Collections.emptyList()); + + userName = params.first("userName"); + sessionId = params.first("sessionId"); + windowTitle = params.firstSafe("windowTitle", "Minecraft"); + + serverAddress = params.firstSafe("serverAddress", null); + serverPort = params.firstSafe("serverPort", null); + + cwd = System.getProperty("user.dir"); + + String windowParams = params.firstSafe("windowParams", "854x480"); + + String[] dimStrings = windowParams.split("x"); + + if (windowParams.equalsIgnoreCase("max")) { + maximize = true; + + winSizeW = DEFAULT_WINDOW_WIDTH; + winSizeH = DEFAULT_WINDOW_HEIGHT; + } else if (dimStrings.length == 2) { + maximize = false; + + winSizeW = Integer.parseInt(dimStrings[0]); + winSizeH = Integer.parseInt(dimStrings[1]); + } else { + throw new IllegalArgumentException("Unexpected window size parameter value: " + windowParams); + } + } + + private void invokeMain(Class mainClass) throws Exception { + Method method = mainClass.getMethod("main", String[].class); + + method.invoke(null, (Object) mcParams.toArray(new String[0])); + } + + private void legacyLaunch() throws Exception { + // Get the Minecraft Class and set the base folder + Class minecraftClass = classLoader.loadClass(mainClass); + + Field baseDirField = Utils.getMinecraftBaseDirField(minecraftClass); + + if (baseDirField == null) { + LOGGER.warning("Could not find Minecraft path field."); + } else { + baseDirField.setAccessible(true); + + baseDirField.set(null, new File(cwd)); + } + + System.setProperty("minecraft.applet.TargetDirectory", cwd); + + if (!traits.contains("noapplet")) { + LOGGER.info("Launching with applet wrapper..."); + + try { + Class mcAppletClass = classLoader.loadClass(appletClass); + + Applet mcApplet = (Applet) mcAppletClass.getConstructor().newInstance(); + + LegacyFrame mcWindow = new LegacyFrame(windowTitle); + + mcWindow.start( + mcApplet, + userName, + sessionId, + winSizeW, + winSizeH, + maximize, + serverAddress, + serverPort + ); + + return; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Applet wrapper failed: ", e); + + LOGGER.warning("Falling back to using main class."); + } + } + + invokeMain(minecraftClass); + } + + void launchWithMainClass() throws Exception { + // window size, title and state, onesix + + // FIXME: there is no good way to maximize the minecraft window in onesix. + // the following often breaks linux screen setups + // mcparams.add("--fullscreen"); + + if (!maximize) { + mcParams.add("--width"); + mcParams.add(Integer.toString(winSizeW)); + mcParams.add("--height"); + mcParams.add(Integer.toString(winSizeH)); + } + + if (serverAddress != null) { + mcParams.add("--server"); + mcParams.add(serverAddress); + mcParams.add("--port"); + mcParams.add(serverPort); + } + + invokeMain(classLoader.loadClass(mainClass)); + } + + @Override + public void launch() throws Exception { + if (traits.contains("legacyLaunch") || traits.contains("alphaLaunch")) { + // legacy launch uses the applet wrapper + legacyLaunch(); + } else { + // normal launch just calls main() + launchWithMainClass(); + } + } + +} diff --git a/libraries/launcher/org/multimc/onesix/OneSixLauncher.java b/libraries/launcher/org/multimc/onesix/OneSixLauncher.java deleted file mode 100644 index 0058bd43..00000000 --- a/libraries/launcher/org/multimc/onesix/OneSixLauncher.java +++ /dev/null @@ -1,256 +0,0 @@ -/* Copyright 2012-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. - */ - -package org.multimc.onesix; - -import org.multimc.*; - -import java.applet.Applet; -import java.io.File; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class OneSixLauncher implements Launcher -{ - - private static final Logger LOGGER = Logger.getLogger("OneSixLauncher"); - - // parameters, separated from ParamBucket - private List libraries; - private List mcparams; - private List mods; - private List jarmods; - private List coremods; - private List traits; - private String appletClass; - private String mainClass; - private String nativePath; - private String userName, sessionId; - private String windowTitle; - private String windowParams; - - // secondary parameters - private int winSizeW; - private int winSizeH; - private boolean maximize; - private String cwd; - - private String serverAddress; - private String serverPort; - - // the much abused system classloader, for convenience (for further abuse) - private ClassLoader cl; - - private void processParams(ParamBucket params) throws NotFoundException - { - libraries = params.all("cp"); - mcparams = params.allSafe("param", new ArrayList() ); - mainClass = params.firstSafe("mainClass", "net.minecraft.client.Minecraft"); - appletClass = params.firstSafe("appletClass", "net.minecraft.client.MinecraftApplet"); - traits = params.allSafe("traits", new ArrayList()); - nativePath = params.first("natives"); - - userName = params.first("userName"); - sessionId = params.first("sessionId"); - windowTitle = params.firstSafe("windowTitle", "Minecraft"); - windowParams = params.firstSafe("windowParams", "854x480"); - - serverAddress = params.firstSafe("serverAddress", null); - serverPort = params.firstSafe("serverPort", null); - - cwd = System.getProperty("user.dir"); - - winSizeW = 854; - winSizeH = 480; - maximize = false; - - String[] dimStrings = windowParams.split("x"); - - if (windowParams.equalsIgnoreCase("max")) - { - maximize = true; - } - else if (dimStrings.length == 2) - { - try - { - winSizeW = Integer.parseInt(dimStrings[0]); - winSizeH = Integer.parseInt(dimStrings[1]); - } catch (NumberFormatException ignored) {} - } - } - - int legacyLaunch() - { - // Get the Minecraft Class and set the base folder - Class mc; - try - { - mc = cl.loadClass(mainClass); - - Field f = Utils.getMCPathField(mc); - - if (f == null) - { - LOGGER.warning("Could not find Minecraft path field."); - } - else - { - f.setAccessible(true); - f.set(null, new File(cwd)); - } - } catch (Exception e) - { - LOGGER.log( - Level.SEVERE, - "Could not set base folder. Failed to find/access Minecraft main class:", - e - ); - - return -1; - } - - System.setProperty("minecraft.applet.TargetDirectory", cwd); - - if(!traits.contains("noapplet")) - { - LOGGER.info("Launching with applet wrapper..."); - try - { - Class MCAppletClass = cl.loadClass(appletClass); - Applet mcappl = (Applet) MCAppletClass.newInstance(); - LegacyFrame mcWindow = new LegacyFrame(windowTitle); - mcWindow.start(mcappl, userName, sessionId, winSizeW, winSizeH, maximize, serverAddress, serverPort); - return 0; - } catch (Exception e) - { - LOGGER.log(Level.SEVERE, "Applet wrapper failed:", e); - - LOGGER.warning("Falling back to using main class."); - } - } - - // init params for the main method to chomp on. - String[] paramsArray = mcparams.toArray(new String[mcparams.size()]); - try - { - mc.getMethod("main", String[].class).invoke(null, (Object) paramsArray); - return 0; - } catch (Exception e) - { - LOGGER.log(Level.SEVERE, "Failed to invoke the Minecraft main class:", e); - - return -1; - } - } - - int launchWithMainClass() - { - // window size, title and state, onesix - if (maximize) - { - // FIXME: there is no good way to maximize the minecraft window in onesix. - // the following often breaks linux screen setups - // mcparams.add("--fullscreen"); - } - else - { - mcparams.add("--width"); - mcparams.add(Integer.toString(winSizeW)); - mcparams.add("--height"); - mcparams.add(Integer.toString(winSizeH)); - } - - if (serverAddress != null) - { - mcparams.add("--server"); - mcparams.add(serverAddress); - mcparams.add("--port"); - mcparams.add(serverPort); - } - - // Get the Minecraft Class. - Class mc; - try - { - mc = cl.loadClass(mainClass); - } catch (ClassNotFoundException e) - { - LOGGER.log(Level.SEVERE, "Failed to find Minecraft main class:", e); - - return -1; - } - - // get the main method. - Method meth; - try - { - meth = mc.getMethod("main", String[].class); - } catch (NoSuchMethodException e) - { - LOGGER.log(Level.SEVERE, "Failed to acquire the main method:", e); - - return -1; - } - - // init params for the main method to chomp on. - String[] paramsArray = mcparams.toArray(new String[mcparams.size()]); - try - { - // static method doesn't have an instance - meth.invoke(null, (Object) paramsArray); - } catch (Exception e) - { - LOGGER.log(Level.SEVERE, "Failed to start Minecraft:", e); - - return -1; - } - return 0; - } - - @Override - public int launch(ParamBucket params) - { - // get and process the launch script params - try - { - processParams(params); - } catch (NotFoundException e) - { - LOGGER.log(Level.SEVERE, "Not enough arguments!"); - - return -1; - } - - // grab the system classloader and ... - cl = ClassLoader.getSystemClassLoader(); - - if (traits.contains("legacyLaunch") || traits.contains("alphaLaunch") ) - { - // legacy launch uses the applet wrapper - return legacyLaunch(); - } - else - { - // normal launch just calls main() - return launchWithMainClass(); - } - } - -} diff --git a/libraries/launcher/org/multimc/ParamBucket.java b/libraries/launcher/org/multimc/utils/ParamBucket.java similarity index 68% rename from libraries/launcher/org/multimc/ParamBucket.java rename to libraries/launcher/org/multimc/utils/ParamBucket.java index 8ff03ddc..26ff8eef 100644 --- a/libraries/launcher/org/multimc/ParamBucket.java +++ b/libraries/launcher/org/multimc/utils/ParamBucket.java @@ -14,36 +14,34 @@ * limitations under the License. */ -package org.multimc; +package org.multimc.utils; + +import org.multimc.exception.ParameterNotFoundException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -public class ParamBucket -{ +public final class ParamBucket { private final Map> paramsMap = new HashMap<>(); - public void add(String key, String value) - { + public void add(String key, String value) { paramsMap.computeIfAbsent(key, k -> new ArrayList<>()) .add(value); } - public List all(String key) throws NotFoundException - { + public List all(String key) throws ParameterNotFoundException { List params = paramsMap.get(key); if (params == null) - throw new NotFoundException(); + throw new ParameterNotFoundException(key); return params; } - public List allSafe(String key, List def) - { + public List allSafe(String key, List def) { List params = paramsMap.get(key); if (params == null || params.isEmpty()) @@ -52,23 +50,16 @@ public class ParamBucket return params; } - public List allSafe(String key) - { - return allSafe(key, new ArrayList<>()); - } - - public String first(String key) throws NotFoundException - { + public String first(String key) throws ParameterNotFoundException { List list = all(key); if (list.isEmpty()) - throw new NotFoundException(); + throw new ParameterNotFoundException(key); return list.get(0); } - public String firstSafe(String key, String def) - { + public String firstSafe(String key, String def) { List params = paramsMap.get(key); if (params == null || params.isEmpty()) @@ -77,9 +68,4 @@ public class ParamBucket return params.get(0); } - public String firstSafe(String key) - { - return firstSafe(key, ""); - } - } diff --git a/libraries/launcher/org/multimc/utils/Utils.java b/libraries/launcher/org/multimc/utils/Utils.java new file mode 100644 index 00000000..416eff26 --- /dev/null +++ b/libraries/launcher/org/multimc/utils/Utils.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-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. + */ + +package org.multimc.utils; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +public final class Utils { + + private Utils() {} + + /** + * Finds a field that looks like a Minecraft base folder in a supplied class + * + * @param clazz the class to scan + */ + public static Field getMinecraftBaseDirField(Class clazz) { + for (Field f : clazz.getDeclaredFields()) { + // Has to be File + if (f.getType() != File.class) + continue; + + // And Private Static. + if (!Modifier.isStatic(f.getModifiers()) || !Modifier.isPrivate(f.getModifiers())) + continue; + + return f; + } + + return null; + } + +} + From eeb5297284494c03f3b8e3927c5ed6cc3ca09a41 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Tue, 3 May 2022 00:25:26 +0100 Subject: [PATCH 331/605] Use only Java 7 features (in order to deal with #515) --- .../launcher/org/multimc/LauncherFactory.java | 23 +++++++++++++------ .../org/multimc/applet/LegacyFrame.java | 21 +++++++++-------- .../org/multimc/impl/OneSixLauncher.java | 4 ++-- .../org/multimc/utils/ParamBucket.java | 11 +++++++-- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/libraries/launcher/org/multimc/LauncherFactory.java b/libraries/launcher/org/multimc/LauncherFactory.java index b5d0dd5b..2b370058 100644 --- a/libraries/launcher/org/multimc/LauncherFactory.java +++ b/libraries/launcher/org/multimc/LauncherFactory.java @@ -5,30 +5,39 @@ import org.multimc.utils.ParamBucket; import java.util.HashMap; import java.util.Map; -import java.util.function.Function; public final class LauncherFactory { private static final LauncherFactory INSTANCE = new LauncherFactory(); - private final Map> launcherRegistry = new HashMap<>(); + private final Map launcherRegistry = new HashMap<>(); private LauncherFactory() { - launcherRegistry.put("onesix", OneSixLauncher::new); + launcherRegistry.put("onesix", new LauncherProvider() { + @Override + public Launcher provide(ParamBucket parameters) { + return new OneSixLauncher(parameters); + } + }); } public Launcher createLauncher(String name, ParamBucket parameters) { - Function launcherCreator = - launcherRegistry.get(name); + LauncherProvider launcherProvider = launcherRegistry.get(name); - if (launcherCreator == null) + if (launcherProvider == null) throw new IllegalArgumentException("Invalid launcher type: " + name); - return launcherCreator.apply(parameters); + return launcherProvider.provide(parameters); } public static LauncherFactory getInstance() { return INSTANCE; } + public interface LauncherProvider { + + Launcher provide(ParamBucket parameters); + + } + } diff --git a/libraries/launcher/org/multimc/applet/LegacyFrame.java b/libraries/launcher/org/multimc/applet/LegacyFrame.java index a5e6c170..d250ce26 100644 --- a/libraries/launcher/org/multimc/applet/LegacyFrame.java +++ b/libraries/launcher/org/multimc/applet/LegacyFrame.java @@ -141,16 +141,19 @@ public final class LegacyFrame extends Frame { @Override public void windowClosing(WindowEvent e) { - new Thread(() -> { - try { - Thread.sleep(30000L); - } catch (InterruptedException localInterruptedException) { - localInterruptedException.printStackTrace(); + new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(30000L); + } catch (InterruptedException localInterruptedException) { + localInterruptedException.printStackTrace(); + } + + LOGGER.info("Forcing exit!"); + + System.exit(0); } - - LOGGER.info("Forcing exit!"); - - System.exit(0); }).start(); if (appletWrap != null) { diff --git a/libraries/launcher/org/multimc/impl/OneSixLauncher.java b/libraries/launcher/org/multimc/impl/OneSixLauncher.java index d2596a69..19253dc0 100644 --- a/libraries/launcher/org/multimc/impl/OneSixLauncher.java +++ b/libraries/launcher/org/multimc/impl/OneSixLauncher.java @@ -58,10 +58,10 @@ public final class OneSixLauncher implements Launcher { public OneSixLauncher(ParamBucket params) { classLoader = ClassLoader.getSystemClassLoader(); - mcParams = params.allSafe("param", Collections.emptyList()); + mcParams = params.allSafe("param", Collections.emptyList()); mainClass = params.firstSafe("mainClass", "net.minecraft.client.Minecraft"); appletClass = params.firstSafe("appletClass", "net.minecraft.client.MinecraftApplet"); - traits = params.allSafe("traits", Collections.emptyList()); + traits = params.allSafe("traits", Collections.emptyList()); userName = params.first("userName"); sessionId = params.first("sessionId"); diff --git a/libraries/launcher/org/multimc/utils/ParamBucket.java b/libraries/launcher/org/multimc/utils/ParamBucket.java index 26ff8eef..5dbb8775 100644 --- a/libraries/launcher/org/multimc/utils/ParamBucket.java +++ b/libraries/launcher/org/multimc/utils/ParamBucket.java @@ -28,8 +28,15 @@ public final class ParamBucket { private final Map> paramsMap = new HashMap<>(); public void add(String key, String value) { - paramsMap.computeIfAbsent(key, k -> new ArrayList<>()) - .add(value); + List params = paramsMap.get(key); + + if (params == null) { + params = new ArrayList<>(); + + paramsMap.put(key, params); + } + + params.add(value); } public List all(String key) throws ParameterNotFoundException { From 4fdb21b41400e789ca44a5cc1079469eb2508370 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Tue, 3 May 2022 00:27:14 +0100 Subject: [PATCH 332/605] Compile with Java 7 in mind --- libraries/launcher/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/launcher/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index e0149482..0a0a541c 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -4,7 +4,7 @@ find_package(Java 1.7 REQUIRED COMPONENTS Development) include(UseJava) set(CMAKE_JAVA_JAR_ENTRY_POINT org.multimc.EntryPoint) -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 org/multimc/EntryPoint.java From 860a7af6796785898926bcf10b034545caa5401b Mon Sep 17 00:00:00 2001 From: icelimetea Date: Tue, 3 May 2022 00:53:22 +0100 Subject: [PATCH 333/605] Fix method access modifier --- libraries/launcher/org/multimc/impl/OneSixLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/launcher/org/multimc/impl/OneSixLauncher.java b/libraries/launcher/org/multimc/impl/OneSixLauncher.java index 19253dc0..a87b116c 100644 --- a/libraries/launcher/org/multimc/impl/OneSixLauncher.java +++ b/libraries/launcher/org/multimc/impl/OneSixLauncher.java @@ -145,7 +145,7 @@ public final class OneSixLauncher implements Launcher { invokeMain(minecraftClass); } - void launchWithMainClass() throws Exception { + private void launchWithMainClass() throws Exception { // window size, title and state, onesix // FIXME: there is no good way to maximize the minecraft window in onesix. From 9a87ae575ef58bb86d4bbd7bdb8ab7e026ad9a33 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Tue, 3 May 2022 03:19:26 +0100 Subject: [PATCH 334/605] More minor fixes --- libraries/launcher/CMakeLists.txt | 2 +- .../launcher/net/minecraft/Launcher.java | 30 ++++------------ .../launcher/org/multimc/EntryPoint.java | 4 +-- .../launcher/org/multimc/LauncherFactory.java | 8 ++--- .../org/multimc/applet/LegacyFrame.java | 25 +++++++------ .../org/multimc/exception/ParseException.java | 4 --- .../org/multimc/impl/OneSixLauncher.java | 36 +++++++++++-------- .../{ParamBucket.java => Parameters.java} | 2 +- 8 files changed, 47 insertions(+), 64 deletions(-) rename libraries/launcher/org/multimc/utils/{ParamBucket.java => Parameters.java} (98%) diff --git a/libraries/launcher/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index 0a0a541c..2c859499 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -14,7 +14,7 @@ set(SRC org/multimc/applet/LegacyFrame.java org/multimc/exception/ParameterNotFoundException.java org/multimc/exception/ParseException.java - org/multimc/utils/ParamBucket.java + org/multimc/utils/Parameters.java org/multimc/utils/Utils.java net/minecraft/Launcher.java ) diff --git a/libraries/launcher/net/minecraft/Launcher.java b/libraries/launcher/net/minecraft/Launcher.java index 04201047..265fa66a 100644 --- a/libraries/launcher/net/minecraft/Launcher.java +++ b/libraries/launcher/net/minecraft/Launcher.java @@ -24,22 +24,20 @@ import java.net.URL; import java.util.Map; import java.util.TreeMap; -public class Launcher extends Applet implements AppletStub { +public final class Launcher extends Applet implements AppletStub { private final Map params = new TreeMap<>(); + private final Applet wrappedApplet; + private boolean active = false; - private Applet wrappedApplet; - private URL documentBase; - - public Launcher(Applet applet, URL documentBase) { + public Launcher(Applet applet) { this.setLayout(new BorderLayout()); this.add(applet, "Center"); this.wrappedApplet = applet; - this.documentBase = documentBase; } public void setParameter(String name, String value) @@ -47,21 +45,6 @@ public class Launcher extends Applet implements AppletStub { params.put(name, value); } - public void replace(Applet applet) { - this.wrappedApplet = applet; - - applet.setStub(this); - applet.setSize(getWidth(), getHeight()); - - this.setLayout(new BorderLayout()); - this.add(applet, "Center"); - - applet.init(); - active = true; - applet.start(); - validate(); - } - @Override public String getParameter(String name) { String param = params.get(name); @@ -135,9 +118,8 @@ public class Launcher extends Applet implements AppletStub { public URL getDocumentBase() { try { // Special case only for Classic versions - if (wrappedApplet.getClass().getCanonicalName().startsWith("com.mojang")) { - return new URL("http", "www.minecraft.net", 80, "/game/", null); - } + if (wrappedApplet.getClass().getCanonicalName().startsWith("com.mojang")) + return new URL("http", "www.minecraft.net", 80, "/game/"); return new URL("http://www.minecraft.net/game/"); } catch (MalformedURLException e) { diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index be06d1b4..416f2189 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -15,7 +15,7 @@ package org.multimc;/* */ import org.multimc.exception.ParseException; -import org.multimc.utils.ParamBucket; +import org.multimc.utils.Parameters; import java.io.BufferedReader; import java.io.IOException; @@ -28,7 +28,7 @@ public final class EntryPoint { private static final Logger LOGGER = Logger.getLogger("EntryPoint"); - private final ParamBucket params = new ParamBucket(); + private final Parameters params = new Parameters(); private String launcherType; diff --git a/libraries/launcher/org/multimc/LauncherFactory.java b/libraries/launcher/org/multimc/LauncherFactory.java index 2b370058..17e0d905 100644 --- a/libraries/launcher/org/multimc/LauncherFactory.java +++ b/libraries/launcher/org/multimc/LauncherFactory.java @@ -1,7 +1,7 @@ package org.multimc; import org.multimc.impl.OneSixLauncher; -import org.multimc.utils.ParamBucket; +import org.multimc.utils.Parameters; import java.util.HashMap; import java.util.Map; @@ -15,13 +15,13 @@ public final class LauncherFactory { private LauncherFactory() { launcherRegistry.put("onesix", new LauncherProvider() { @Override - public Launcher provide(ParamBucket parameters) { + public Launcher provide(Parameters parameters) { return new OneSixLauncher(parameters); } }); } - public Launcher createLauncher(String name, ParamBucket parameters) { + public Launcher createLauncher(String name, Parameters parameters) { LauncherProvider launcherProvider = launcherRegistry.get(name); if (launcherProvider == null) @@ -36,7 +36,7 @@ public final class LauncherFactory { public interface LauncherProvider { - Launcher provide(ParamBucket parameters); + Launcher provide(Parameters parameters); } diff --git a/libraries/launcher/org/multimc/applet/LegacyFrame.java b/libraries/launcher/org/multimc/applet/LegacyFrame.java index d250ce26..c50995f6 100644 --- a/libraries/launcher/org/multimc/applet/LegacyFrame.java +++ b/libraries/launcher/org/multimc/applet/LegacyFrame.java @@ -23,8 +23,6 @@ import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -38,11 +36,15 @@ public final class LegacyFrame extends Frame { private static final Logger LOGGER = Logger.getLogger("LegacyFrame"); - private Launcher appletWrap; + private final Launcher appletWrap; - public LegacyFrame(String title) { + public LegacyFrame(String title, Applet mcApplet) { super(title); + appletWrap = new Launcher(mcApplet); + + mcApplet.setStub(appletWrap); + try { setIconImage(ImageIO.read(new File("icon.png"))); } catch (IOException e) { @@ -53,7 +55,6 @@ public final class LegacyFrame extends Frame { } public void start ( - Applet mcApplet, String user, String session, int winSizeW, @@ -62,14 +63,14 @@ public final class LegacyFrame extends Frame { String serverAddress, String serverPort ) { - try { - appletWrap = new Launcher(mcApplet, new URL("http://www.minecraft.net/game")); - } catch (MalformedURLException ignored) {} - // Implements support for launching in to multiplayer on classic servers using a mpticket // file generated by an external program and stored in the instance's root folder. - Path mpticketFile = Paths.get(System.getProperty("user.dir") + "/../mpticket"); - Path mpticketFileCorrupt = Paths.get(System.getProperty("user.dir") + "/../mpticket.corrupt"); + + Path mpticketFile = + Paths.get(System.getProperty("user.dir"), "..", "mpticket"); + + Path mpticketFileCorrupt = + Paths.get(System.getProperty("user.dir"), "..", "mpticket.corrupt"); if (Files.exists(mpticketFile)) { try (Scanner fileScanner = new Scanner( @@ -115,8 +116,6 @@ public final class LegacyFrame extends Frame { appletWrap.setParameter("demo", "false"); appletWrap.setParameter("fullscreen", "false"); - mcApplet.setStub(appletWrap); - add(appletWrap); appletWrap.setPreferredSize(new Dimension(winSizeW, winSizeH)); diff --git a/libraries/launcher/org/multimc/exception/ParseException.java b/libraries/launcher/org/multimc/exception/ParseException.java index c9a4c856..848b395d 100644 --- a/libraries/launcher/org/multimc/exception/ParseException.java +++ b/libraries/launcher/org/multimc/exception/ParseException.java @@ -18,10 +18,6 @@ package org.multimc.exception; public final class ParseException extends IllegalArgumentException { - public ParseException() { - super(); - } - public ParseException(String message) { super(message); } diff --git a/libraries/launcher/org/multimc/impl/OneSixLauncher.java b/libraries/launcher/org/multimc/impl/OneSixLauncher.java index a87b116c..b981e4ff 100644 --- a/libraries/launcher/org/multimc/impl/OneSixLauncher.java +++ b/libraries/launcher/org/multimc/impl/OneSixLauncher.java @@ -17,7 +17,7 @@ package org.multimc.impl; import org.multimc.Launcher; import org.multimc.applet.LegacyFrame; -import org.multimc.utils.ParamBucket; +import org.multimc.utils.Parameters; import org.multimc.utils.Utils; import java.applet.Applet; @@ -55,7 +55,7 @@ public final class OneSixLauncher implements Launcher { private final ClassLoader classLoader; - public OneSixLauncher(ParamBucket params) { + public OneSixLauncher(Parameters params) { classLoader = ClassLoader.getSystemClassLoader(); mcParams = params.allSafe("param", Collections.emptyList()); @@ -72,22 +72,29 @@ public final class OneSixLauncher implements Launcher { cwd = System.getProperty("user.dir"); - String windowParams = params.firstSafe("windowParams", "854x480"); + String windowParams = params.firstSafe("windowParams", null); - String[] dimStrings = windowParams.split("x"); + if (windowParams != null) { + String[] dimStrings = windowParams.split("x"); - if (windowParams.equalsIgnoreCase("max")) { - maximize = true; + if (windowParams.equalsIgnoreCase("max")) { + maximize = true; + + winSizeW = DEFAULT_WINDOW_WIDTH; + winSizeH = DEFAULT_WINDOW_HEIGHT; + } else if (dimStrings.length == 2) { + maximize = false; + + winSizeW = Integer.parseInt(dimStrings[0]); + winSizeH = Integer.parseInt(dimStrings[1]); + } else { + throw new IllegalArgumentException("Unexpected window size parameter value: " + windowParams); + } + } else { + maximize = false; winSizeW = DEFAULT_WINDOW_WIDTH; winSizeH = DEFAULT_WINDOW_HEIGHT; - } else if (dimStrings.length == 2) { - maximize = false; - - winSizeW = Integer.parseInt(dimStrings[0]); - winSizeH = Integer.parseInt(dimStrings[1]); - } else { - throw new IllegalArgumentException("Unexpected window size parameter value: " + windowParams); } } @@ -121,10 +128,9 @@ public final class OneSixLauncher implements Launcher { Applet mcApplet = (Applet) mcAppletClass.getConstructor().newInstance(); - LegacyFrame mcWindow = new LegacyFrame(windowTitle); + LegacyFrame mcWindow = new LegacyFrame(windowTitle, mcApplet); mcWindow.start( - mcApplet, userName, sessionId, winSizeW, diff --git a/libraries/launcher/org/multimc/utils/ParamBucket.java b/libraries/launcher/org/multimc/utils/Parameters.java similarity index 98% rename from libraries/launcher/org/multimc/utils/ParamBucket.java rename to libraries/launcher/org/multimc/utils/Parameters.java index 5dbb8775..7be790c2 100644 --- a/libraries/launcher/org/multimc/utils/ParamBucket.java +++ b/libraries/launcher/org/multimc/utils/Parameters.java @@ -23,7 +23,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -public final class ParamBucket { +public final class Parameters { private final Map> paramsMap = new HashMap<>(); From e909cc363d2236ad99601222728bad5b1ea71c31 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Wed, 27 Apr 2022 15:55:34 +0800 Subject: [PATCH 335/605] add big sur-style icon --- program_info/genicons.sh | 23 +++++++++++++--- program_info/org.polymc.PolyMC.bigsur.svg | 32 ++++++++++++++++++++++ program_info/polymc.icns | Bin 272578 -> 261369 bytes 3 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 program_info/org.polymc.PolyMC.bigsur.svg diff --git a/program_info/genicons.sh b/program_info/genicons.sh index b553edb5..313bdb53 100755 --- a/program_info/genicons.sh +++ b/program_info/genicons.sh @@ -1,5 +1,7 @@ #/bin/bash +# ICO + inkscape -w 16 -h 16 -o polymc_16.png org.polymc.PolyMC.svg inkscape -w 24 -h 24 -o polymc_24.png org.polymc.PolyMC.svg inkscape -w 32 -h 32 -o polymc_32.png org.polymc.PolyMC.svg @@ -9,11 +11,24 @@ inkscape -w 128 -h 128 -o polymc_128.png org.polymc.PolyMC.svg convert polymc_128.png polymc_64.png polymc_48.png polymc_32.png polymc_24.png polymc_16.png polymc.ico -inkscape -w 256 -h 256 -o polymc_256.png org.polymc.PolyMC.svg -inkscape -w 512 -h 512 -o polymc_512.png org.polymc.PolyMC.svg -inkscape -w 1024 -h 1024 -o polymc_1024.png org.polymc.PolyMC.svg +rm -f polymc_*.png -png2icns polymc.icns polymc_1024.png polymc_512.png polymc_256.png polymc_128.png polymc_32.png polymc_16.png +inkscape -w 1024 -h 1024 -o polymc_1024.png org.polymc.PolyMC.bigsur.svg + +mkdir polymc.iconset + +sips -z 16 16 polymc_1024.png --out polymc.iconset/icon_16x16.png +sips -z 32 32 polymc_1024.png --out polymc.iconset/icon_16x16@2x.png +sips -z 32 32 polymc_1024.png --out polymc.iconset/icon_32x32.png +sips -z 64 64 polymc_1024.png --out polymc.iconset/icon_32x32@2x.png +sips -z 128 128 polymc_1024.png --out polymc.iconset/icon_128x128.png +sips -z 256 256 polymc_1024.png --out polymc.iconset/icon_128x128@2x.png +sips -z 256 256 polymc_1024.png --out polymc.iconset/icon_256x256.png +sips -z 512 512 polymc_1024.png --out polymc.iconset/icon_256x256@2x.png +sips -z 512 512 polymc_1024.png --out polymc.iconset/icon_512x512.png +cp polymc_1024.png polymc.iconset/icon_512x512@2x.png + +iconutil -c icns polymc.iconset rm -f polymc_*.png rm -rf polymc.iconset diff --git a/program_info/org.polymc.PolyMC.bigsur.svg b/program_info/org.polymc.PolyMC.bigsur.svg new file mode 100644 index 00000000..1d680032 --- /dev/null +++ b/program_info/org.polymc.PolyMC.bigsur.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/program_info/polymc.icns b/program_info/polymc.icns index 84148d1a1877c2dca145124c48a225821735fa8f..a090c1b0d4be086a066f82a1f21514ccb974be0f 100644 GIT binary patch literal 261369 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 literal 272578 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ⅇ#FG~J7cDnHn%MJU+Pxt~` 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 Date: Thu, 5 May 2022 07:14:32 +0100 Subject: [PATCH 336/605] Improve mpticket file parsing code --- .../org/multimc/applet/LegacyFrame.java | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/libraries/launcher/org/multimc/applet/LegacyFrame.java b/libraries/launcher/org/multimc/applet/LegacyFrame.java index c50995f6..e3bd5047 100644 --- a/libraries/launcher/org/multimc/applet/LegacyFrame.java +++ b/libraries/launcher/org/multimc/applet/LegacyFrame.java @@ -28,7 +28,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.util.Scanner; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -51,7 +51,7 @@ public final class LegacyFrame extends Frame { LOGGER.log(Level.WARNING, "Unable to read Minecraft icon!", e); } - this.addWindowListener(new ForceExitHandler()); + addWindowListener(new ForceExitHandler()); } public void start ( @@ -73,34 +73,24 @@ public final class LegacyFrame extends Frame { Paths.get(System.getProperty("user.dir"), "..", "mpticket.corrupt"); if (Files.exists(mpticketFile)) { - try (Scanner fileScanner = new Scanner( - Files.newInputStream(mpticketFile), - StandardCharsets.US_ASCII.name() - )) { - String[] mpticketParams = new String[3]; + try { + List lines = Files.readAllLines(mpticketFile, StandardCharsets.UTF_8); - for (int i = 0; i < mpticketParams.length; i++) { - if (fileScanner.hasNextLine()) { - mpticketParams[i] = fileScanner.nextLine(); - } else { - Files.move( - mpticketFile, - mpticketFileCorrupt, - StandardCopyOption.REPLACE_EXISTING - ); + if (lines.size() != 3) { + Files.move( + mpticketFile, + mpticketFileCorrupt, + StandardCopyOption.REPLACE_EXISTING + ); - throw new IllegalArgumentException("Mpticket file is corrupted!"); - } + LOGGER.warning("Mpticket file is corrupted!"); + } else { + appletWrap.setParameter("server", lines.get(0)); + appletWrap.setParameter("port", lines.get(1)); + appletWrap.setParameter("mppass", lines.get(2)); } - - Files.delete(mpticketFile); - - // Assumes parameters are valid and in the correct order - appletWrap.setParameter("server", mpticketParams[0]); - appletWrap.setParameter("port", mpticketParams[1]); - appletWrap.setParameter("mppass", mpticketParams[2]); } catch (IOException e) { - LOGGER.log(Level.WARNING, "Unable to read mpticket file!", e); + LOGGER.log(Level.WARNING, "Unable to red mpticket file!", e); } } From 6bffa060637e3620739344925a4681ec494a725b Mon Sep 17 00:00:00 2001 From: icelimetea Date: Thu, 5 May 2022 07:16:16 +0100 Subject: [PATCH 337/605] Fix typo --- libraries/launcher/org/multimc/applet/LegacyFrame.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/launcher/org/multimc/applet/LegacyFrame.java b/libraries/launcher/org/multimc/applet/LegacyFrame.java index e3bd5047..0283f92c 100644 --- a/libraries/launcher/org/multimc/applet/LegacyFrame.java +++ b/libraries/launcher/org/multimc/applet/LegacyFrame.java @@ -85,12 +85,13 @@ public final class LegacyFrame extends Frame { LOGGER.warning("Mpticket file is corrupted!"); } else { + // Assumes parameters are valid and in the correct order appletWrap.setParameter("server", lines.get(0)); appletWrap.setParameter("port", lines.get(1)); appletWrap.setParameter("mppass", lines.get(2)); } } catch (IOException e) { - LOGGER.log(Level.WARNING, "Unable to red mpticket file!", e); + LOGGER.log(Level.WARNING, "Unable to read mpticket file!", e); } } From 113528e1f299de951a7223df033bbf390095dba3 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Thu, 5 May 2022 07:20:33 +0100 Subject: [PATCH 338/605] Make line count check more lenient --- libraries/launcher/org/multimc/applet/LegacyFrame.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/launcher/org/multimc/applet/LegacyFrame.java b/libraries/launcher/org/multimc/applet/LegacyFrame.java index 0283f92c..f82cb605 100644 --- a/libraries/launcher/org/multimc/applet/LegacyFrame.java +++ b/libraries/launcher/org/multimc/applet/LegacyFrame.java @@ -76,7 +76,7 @@ public final class LegacyFrame extends Frame { try { List lines = Files.readAllLines(mpticketFile, StandardCharsets.UTF_8); - if (lines.size() != 3) { + if (lines.size() < 3) { Files.move( mpticketFile, mpticketFileCorrupt, From 2fbb7be23bb31d0c5007a0500ad2a5d3a51f644e Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 7 May 2022 20:16:55 -0300 Subject: [PATCH 339/605] fix: filter based on MIME type instead of plaintext suffix Suffixes are unreliable in different locales, while MIME types are more standarized. --- launcher/ui/dialogs/SkinUploadDialog.cpp | 3 ++- launcher/ui/pages/modplatform/ImportPage.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp index 6a5a324f..8d137afc 100644 --- a/launcher/ui/dialogs/SkinUploadDialog.cpp +++ b/launcher/ui/dialogs/SkinUploadDialog.cpp @@ -100,7 +100,8 @@ void SkinUploadDialog::on_buttonBox_accepted() void SkinUploadDialog::on_skinBrowseBtn_clicked() { - QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), "*.png"); + auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString(); + QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter); if (raw_path.isEmpty() || !QFileInfo::exists(raw_path)) { return; diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index 487bf77b..1b53dd40 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -143,7 +143,8 @@ void ImportPage::setUrl(const QString& url) void ImportPage::on_modpackBtn_clicked() { - const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), tr("Zip (*.zip)")); + auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); + const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); if (url.isValid()) { if (url.isLocalFile()) From 29a53d7e95508f6c7cd6c1945d2100cca98533c1 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 7 May 2022 20:42:19 -0300 Subject: [PATCH 340/605] fix: always have the instance toolbar be vertical This overrides the orientation set automatically by Qt when we start moving the toolbar around. --- launcher/ui/MainWindow.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index f34cf1ab..44eba369 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -746,6 +746,9 @@ public: // disabled until we have an instance selected instanceToolBar->setEnabled(false); instanceToolBar->setMovable(true); + // Qt doesn't like vertical moving toolbars, so we have to force them... + // See https://github.com/PolyMC/PolyMC/issues/493 + connect(instanceToolBar, &QToolBar::orientationChanged, [=](Qt::Orientation){ instanceToolBar->setOrientation(Qt::Vertical); }); instanceToolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea); instanceToolBar->setToolButtonStyle(Qt::ToolButtonTextOnly); instanceToolBar->setFloatable(false); From ae1aa6f63eb5f82788b3ff638becd8b6a9a44c74 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Sun, 8 May 2022 15:02:21 +0800 Subject: [PATCH 341/605] gitignore stuff --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c9a762f5..2a715656 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ CMakeLists.txt.user.* /.project /.settings /.idea +/.vscode +.clang-format cmake-build-*/ Debug From 22f5128e398f34f01f78751008aca594fa7a7eee Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Sun, 8 May 2022 15:22:50 +0800 Subject: [PATCH 342/605] adopt changes from #497 remapped --- CMakeLists.txt | 4 + buildconfig/BuildConfig.cpp.in | 1 + buildconfig/BuildConfig.h | 12 +- .../modplatform/flame/FileResolvingTask.cpp | 34 ++--- launcher/modplatform/flame/FlameAPI.h | 32 ++++- launcher/modplatform/flame/FlameModIndex.cpp | 52 ++----- launcher/modplatform/flame/FlamePackIndex.cpp | 50 +++---- launcher/modplatform/flame/PackManifest.cpp | 87 ++++-------- launcher/net/Download.cpp | 114 ++++++---------- launcher/ui/pages/modplatform/ModModel.cpp | 49 ++++--- .../pages/modplatform/flame/FlameModModel.cpp | 4 +- .../ui/pages/modplatform/flame/FlameModel.cpp | 129 +++++++----------- .../ui/pages/modplatform/flame/FlamePage.cpp | 71 ++++------ 13 files changed, 261 insertions(+), 378 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a7824a99..d1c20bb0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,10 @@ set(Launcher_IMGUR_CLIENT_ID "5b97b0713fba4a3" CACHE STRING "Client ID you can g # MSA Client ID set(Launcher_MSA_CLIENT_ID "549033b2-1532-4d4e-ae77-1bbaa46f9d74" CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application") +# CurseForge API Key +# CHANGE THIS IF YOU FORK THIS PROJECT! +set(Launcher_CURSEFORGE_API_KEY "$2a$10$iR1RdPDG95FWdILZbHuoMOlV4vL4eckBx7QPZR6SVZmliEb9ZQplu" CACHE STRING "CurseForge API Key") + # Bug tracker URL set(Launcher_BUG_TRACKER_URL "https://github.com/PolyMC/PolyMC/issues" CACHE STRING "URL for the bug tracker.") diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 7360d964..70f8f7f0 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -90,6 +90,7 @@ Config::Config() HELP_URL = "@Launcher_HELP_URL@"; IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@"; MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@"; + CURSEFORGE_API_KEY = "@Launcher_CURSEFORGE_API_KEY@"; META_URL = "@Launcher_META_URL@"; BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@"; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 6304387c..a653e3cf 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -39,9 +39,8 @@ /** * \brief The Config class holds all the build-time information passed from the build system. */ -class Config -{ -public: +class Config { + public: Config(); QString LAUNCHER_NAME; QString LAUNCHER_DISPLAYNAME; @@ -74,7 +73,6 @@ public: /// URL for the updater's channel QString UPDATER_BASE; - /// User-Agent to use. QString USER_AGENT; @@ -116,6 +114,11 @@ public: */ QString MSA_CLIENT_ID; + /** + * Client API key for CurseForge + */ + QString CURSEFORGE_API_KEY; + /** * Metadata repository URL prefix */ @@ -154,4 +157,3 @@ public: }; extern const Config BuildConfig; - diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 3889a935..95924a68 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -1,14 +1,9 @@ #include "FileResolvingTask.h" #include "Json.h" -namespace { - const char * metabase = "https://cursemeta.dries007.net"; -} - Flame::FileResolvingTask::FileResolvingTask(shared_qobject_ptr network, Flame::Manifest& toProcess) : m_network(network), m_toProcess(toProcess) -{ -} +{} void Flame::FileResolvingTask::executeTask() { @@ -17,14 +12,13 @@ void Flame::FileResolvingTask::executeTask() m_dljob = new NetJob("Mod id resolver", m_network); results.resize(m_toProcess.files.size()); int index = 0; - for(auto & file: m_toProcess.files) - { + for (auto& file : m_toProcess.files) { auto projectIdStr = QString::number(file.projectId); auto fileIdStr = QString::number(file.fileId); - QString metaurl = QString("%1/%2/%3.json").arg(metabase, projectIdStr, fileIdStr); + QString metaurl = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(projectIdStr, fileIdStr); auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]); m_dljob->addNetAction(dl); - index ++; + index++; } connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished); m_dljob->start(); @@ -34,16 +28,11 @@ void Flame::FileResolvingTask::netJobFinished() { bool failed = false; int index = 0; - for(auto & bytes: results) - { - auto & out = m_toProcess.files[index]; - try - { + for (auto& bytes : results) { + auto& out = m_toProcess.files[index]; + try { failed &= (!out.parseFromBytes(bytes)); - } - catch (const JSONValidationError &e) - { - + } catch (const JSONValidationError& e) { qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:"; qCritical() << e.cause(); qCritical() << "JSON:"; @@ -52,12 +41,9 @@ void Flame::FileResolvingTask::netJobFinished() } index++; } - if(!failed) - { + if (!failed) { emitSucceeded(); - } - else - { + } else { emitFailed(tr("Some mod ID resolving tasks failed.")); } } diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index ce02df65..61628e60 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -3,33 +3,53 @@ #include "modplatform/helpers/NetworkModAPI.h" class FlameAPI : public NetworkModAPI { + private: + inline auto getSortFieldInt(QString sortString) const -> int + { + return sortString == "Featured" ? 1 + : sortString == "Popularity" ? 2 + : sortString == "LastUpdated" ? 3 + : sortString == "Name" ? 4 + : sortString == "Author" ? 5 + : sortString == "TotalDownloads" ? 6 + : sortString == "Category" ? 7 + : sortString == "GameVersion" ? 8 + : 1; + } + private: inline auto getModSearchURL(SearchArgs& args) const -> QString override { auto gameVersionStr = args.versions.size() != 0 ? QString("gameVersion=%1").arg(args.versions.front().toString()) : QString(); return QString( - "https://addons-ecs.forgesvc.net/api/v2/addon/search?" + "https://api.curseforge.com/v1/mods/search?" "gameId=432&" - "categoryId=0&" - "sectionId=6&" + "classId=6&" "index=%1&" "pageSize=25&" "searchFilter=%2&" - "sort=%3&" + "sortField=%3&" + "sortOrder=desc&" "modLoaderType=%4&" "%5") .arg(args.offset) .arg(args.search) - .arg(args.sorting) + .arg(getSortFieldInt(args.sorting)) .arg(getMappedModLoader(args.mod_loader)) .arg(gameVersionStr); }; inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { - return QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(args.addonId); + QString gameVersionQuery = args.mcVersions.size() == 1 ? QString("gameVersion=%1&").arg(args.mcVersions.front().toString()) : ""; + QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loader)); + + return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&%2%3") + .arg(args.addonId) + .arg(gameVersionQuery) + .arg(modLoaderQuery); }; public: diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index c7b86b5c..6334e88c 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -10,23 +10,12 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); pack.name = Json::requireString(obj, "name"); - pack.websiteUrl = Json::ensureString(obj, "websiteUrl", ""); + pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.description = Json::ensureString(obj, "summary", ""); - bool thumbnailFound = false; - auto attachments = Json::requireArray(obj, "attachments"); - for (auto attachmentRaw : attachments) { - auto attachmentObj = Json::requireObject(attachmentRaw); - bool isDefault = attachmentObj.value("isDefault").toBool(false); - if (isDefault) { - thumbnailFound = true; - pack.logoName = Json::requireString(attachmentObj, "title"); - pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl"); - break; - } - } - - if (!thumbnailFound) { throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); } + QJsonObject logo = Json::requireObject(obj, "logo"); + pack.logoName = Json::requireString(logo, "title"); + pack.logoUrl = Json::requireString(logo, "thumbnailUrl"); auto authors = Json::requireArray(obj, "authors"); for (auto authorIter : authors) { @@ -45,18 +34,22 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, { QVector unsortedVersions; auto profile = (dynamic_cast(inst))->getPackProfile(); - bool hasFabric = FlameAPI::getMappedModLoader(profile->getModLoader()) == ModAPI::Fabric; QString mcVersion = profile->getComponentVersion("net.minecraft"); for (auto versionIter : arr) { auto obj = versionIter.toObject(); - auto versionArray = Json::requireArray(obj, "gameVersion"); - if (versionArray.isEmpty()) { continue; } + auto versionArray = Json::requireArray(obj, "gameVersions"); + if (versionArray.isEmpty()) { + continue; + } ModPlatform::IndexedVersion file; for (auto mcVer : versionArray) { - file.mcVersion.append(mcVer.toString()); + auto str = mcVer.toString(); + + if (str.indexOf('.') > -1) + file.mcVersion.append(mcVer.toString()); } file.addonId = pack.addonId; @@ -66,28 +59,9 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, file.downloadUrl = Json::requireString(obj, "downloadUrl"); file.fileName = Json::requireString(obj, "fileName"); - auto modules = Json::requireArray(obj, "modules"); - bool is_valid_fabric_version = false; - for (auto m : modules) { - auto fname = Json::requireString(m.toObject(), "foldername"); - // FIXME: This does not work properly when a mod supports more than one mod loader, since - // FIXME: This also doesn't deal with Quilt mods at the moment - // they bundle the meta files for all of them in the same arquive, even when that version - // doesn't support the given mod loader. - if (hasFabric) { - if (fname == "fabric.mod.json") { - is_valid_fabric_version = true; - break; - } - } else - break; - // NOTE: Since we're not validating forge versions, we can just skip this loop. - } - - if (hasFabric && !is_valid_fabric_version) continue; - 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; diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index 3d8ea22a..549cace6 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -2,76 +2,63 @@ #include "Json.h" -void Flame::loadIndexedPack(Flame::IndexedPack & pack, QJsonObject & obj) +void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); pack.name = Json::requireString(obj, "name"); pack.websiteUrl = Json::ensureString(obj, "websiteUrl", ""); pack.description = Json::ensureString(obj, "summary", ""); - bool thumbnailFound = false; - auto attachments = Json::requireArray(obj, "attachments"); - for(auto attachmentRaw: attachments) { - auto attachmentObj = Json::requireObject(attachmentRaw); - bool isDefault = attachmentObj.value("isDefault").toBool(false); - if(isDefault) { - thumbnailFound = true; - pack.logoName = Json::requireString(attachmentObj, "title"); - pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl"); - break; - } - } - - if(!thumbnailFound) { - throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); - } + auto logo = Json::requireObject(obj, "logo"); + pack.logoName = Json::requireString(logo, "title"); + pack.logoUrl = Json::requireString(logo, "thumbnailUrl"); auto authors = Json::requireArray(obj, "authors"); - for(auto authorIter: 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, "defaultFileId"); + 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) { + 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) { + if (id != defaultFileId) { continue; } - auto versionArray = Json::requireArray(file, "gameVersion"); - if(versionArray.size() < 1) { + auto versionArray = Json::requireArray(file, "gameVersions"); + if (versionArray.size() < 1) { continue; } found = true; break; } - if(!found) { + if (!found) { throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name)); } } -void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr) +void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) { QVector unsortedVersions; - for(auto versionIter: arr) { + for (auto versionIter : arr) { auto version = Json::requireObject(versionIter); - Flame::IndexedVersion file; + Flame::IndexedVersion file; file.addonId = pack.addonId; file.fileId = Json::requireInteger(version, "id"); - auto versionArray = Json::requireArray(version, "gameVersion"); - if(versionArray.size() < 1) { + auto versionArray = Json::requireArray(version, "gameVersions"); + if (versionArray.size() < 1) { continue; } @@ -82,10 +69,7 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr) unsortedVersions.append(file); } - auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool - { - return a.fileId > b.fileId; - }; + 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/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index b928fd16..e4f90c1a 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -1,28 +1,27 @@ #include "PackManifest.h" #include "Json.h" -static void loadFileV1(Flame::File & f, QJsonObject & file) +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); } -static void loadModloaderV1(Flame::Modloader & m, QJsonObject & modLoader) +static void loadModloaderV1(Flame::Modloader& m, QJsonObject& modLoader) { m.id = Json::requireString(modLoader, "id"); m.primary = Json::ensureBoolean(modLoader, QString("primary"), false); } -static void loadMinecraftV1(Flame::Minecraft & m, QJsonObject & minecraft) +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()); - for (QJsonValueRef item : arr) - { + for (QJsonValueRef item : arr) { auto obj = Json::requireObject(item); Flame::Modloader loader; loadModloaderV1(loader, obj); @@ -30,16 +29,15 @@ static void loadMinecraftV1(Flame::Minecraft & m, QJsonObject & minecraft) } } -static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest) +static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest) { auto mc = Json::requireObject(manifest, "minecraft"); loadMinecraftV1(m.minecraft, mc); m.name = Json::ensureString(manifest, QString("name"), "Unnamed"); m.version = Json::ensureString(manifest, QString("version"), QString()); - m.author = Json::ensureString(manifest, QString("author"), "Anonymous Coward"); + m.author = Json::ensureString(manifest, QString("author"), "Anonymous"); auto arr = Json::ensureArray(manifest, "files", QJsonArray()); - for (QJsonValueRef item : arr) - { + for (QJsonValueRef item : arr) { auto obj = Json::requireObject(item); Flame::File file; loadFileV1(file, obj); @@ -48,18 +46,16 @@ static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest) m.overrides = Json::ensureString(manifest, "overrides", "overrides"); } -void Flame::loadManifest(Flame::Manifest & m, const QString &filepath) +void Flame::loadManifest(Flame::Manifest& m, const QString& filepath) { auto doc = Json::requireDocument(filepath); auto obj = Json::requireObject(doc); m.manifestType = Json::requireString(obj, "manifestType"); - if(m.manifestType != "minecraftModpack") - { + if (m.manifestType != "minecraftModpack") { throw JSONValidationError("Not a modpack manifest!"); } m.manifestVersion = Json::requireInteger(obj, "manifestVersion"); - if(m.manifestVersion != 1) - { + if (m.manifestVersion != 1) { throw JSONValidationError(QString("Unknown manifest version (%1)").arg(m.manifestVersion)); } loadManifestV1(m, obj); @@ -68,59 +64,30 @@ void Flame::loadManifest(Flame::Manifest & m, const QString &filepath) bool Flame::File::parseFromBytes(const QByteArray& bytes) { auto doc = Json::requireDocument(bytes); - auto obj = Json::requireObject(doc); - // result code signifies true failure. - if(obj.contains("code")) - { - qCritical() << "Resolving of" << projectId << fileId << "failed because of a negative result:"; - qCritical() << bytes; - return false; + if (!doc.isObject()) { + throw JSONValidationError(QString("data is not an object? that's not supposed to happen")); } - fileName = Json::requireString(obj, "FileNameOnDisk"); - QString rawUrl = Json::requireString(obj, "DownloadURL"); + auto obj = Json::ensureObject(doc.object(), "data"); + + fileName = Json::requireString(obj, "fileName"); + + QString rawUrl = Json::requireString(obj, "downloadUrl"); url = QUrl(rawUrl, QUrl::TolerantMode); - if(!url.isValid()) - { + if (!url.isValid()) { throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); } // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience // It is also optional - QJsonObject projObj = Json::ensureObject(obj, "_Project", {}); - if(!projObj.isEmpty()) - { - QString strType = Json::ensureString(projObj, "PackageType", "mod").toLower(); - if(strType == "singlefile") - { - type = File::Type::SingleFile; - } - else if(strType == "ctoc") - { - type = File::Type::Ctoc; - } - else if(strType == "cmod2") - { - type = File::Type::Cmod2; - } - else if(strType == "mod") - { - type = File::Type::Mod; - } - else if(strType == "folder") - { - type = File::Type::Folder; - } - else if(strType == "modpack") - { - type = File::Type::Modpack; - } - else - { - qCritical() << "Resolving of" << projectId << fileId << "failed because of unknown file type:" << strType; - type = File::Type::Unknown; - return false; - } - targetFolder = Json::ensureString(projObj, "Path", "mods"); + type = File::Type::SingleFile; + + if (fileName.endsWith(".zip")) { + // this is probably a resource pack + targetFolder = "resourcepacks"; + } else { + // this is probably a mod, dunno what else could modpacks download + targetFolder = "mods"; } + resolved = true; return true; } diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index b314573f..65cc8f67 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -15,27 +15,27 @@ #include "Download.h" -#include #include #include +#include -#include "FileSystem.h" -#include "ChecksumValidator.h" -#include "MetaCacheSink.h" #include "ByteArraySink.h" +#include "ChecksumValidator.h" +#include "FileSystem.h" +#include "MetaCacheSink.h" #include "BuildConfig.h" namespace Net { -Download::Download():NetAction() +Download::Download() : NetAction() { m_status = Job_NotStarted; } Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) { - Download * dl = new Download(); + Download* dl = new Download(); dl->m_url = url; dl->m_options = options; auto md5Node = new ChecksumValidator(QCryptographicHash::Md5); @@ -45,9 +45,9 @@ Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options return dl; } -Download::Ptr Download::makeByteArray(QUrl url, QByteArray *output, Options options) +Download::Ptr Download::makeByteArray(QUrl url, QByteArray* output, Options options) { - Download * dl = new Download(); + Download* dl = new Download(); dl->m_url = url; dl->m_options = options; dl->m_sink.reset(new ByteArraySink(output)); @@ -56,30 +56,28 @@ Download::Ptr Download::makeByteArray(QUrl url, QByteArray *output, Options opti Download::Ptr Download::makeFile(QUrl url, QString path, Options options) { - Download * dl = new Download(); + Download* dl = new Download(); dl->m_url = url; dl->m_options = options; dl->m_sink.reset(new FileSink(path)); return dl; } -void Download::addValidator(Validator * v) +void Download::addValidator(Validator* v) { m_sink->addValidator(v); } void Download::startImpl() { - if(m_status == Job_Aborted) - { + if (m_status == Job_Aborted) { qWarning() << "Attempt to start an aborted Download:" << m_url.toString(); emit aborted(m_index_within_job); return; } QNetworkRequest request(m_url); m_status = m_sink->init(request); - switch(m_status) - { + switch (m_status) { case Job_Finished: emit succeeded(m_index_within_job); qDebug() << "Download cache hit " << m_url.toString(); @@ -87,7 +85,7 @@ void Download::startImpl() case Job_InProgress: qDebug() << "Downloading " << m_url.toString(); break; - case Job_Failed_Proceed: // this is meaningless in this context. We do need a sink. + case Job_Failed_Proceed: // this is meaningless in this context. We do need a sink. case Job_NotStarted: case Job_Failed: emit failed(m_index_within_job); @@ -97,8 +95,11 @@ void Download::startImpl() } request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); + if (request.url().host().contains("api.curseforge.com")) { + request.setRawHeader("x-api-key", BuildConfig.CURSEFORGE_API_KEY.toUtf8()); + }; - QNetworkReply *rep = m_network->get(request); + QNetworkReply* rep = m_network->get(request); m_reply.reset(rep); connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64))); @@ -117,17 +118,12 @@ void Download::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) void Download::downloadError(QNetworkReply::NetworkError error) { - if(error == QNetworkReply::OperationCanceledError) - { + if (error == QNetworkReply::OperationCanceledError) { qCritical() << "Aborted " << m_url.toString(); m_status = Job_Aborted; - } - else - { - if(m_options & Option::AcceptLocalFiles) - { - if(m_sink->hasLocalData()) - { + } else { + if (m_options & Option::AcceptLocalFiles) { + if (m_sink->hasLocalData()) { m_status = Job_Failed_Proceed; return; } @@ -138,11 +134,10 @@ void Download::downloadError(QNetworkReply::NetworkError error) } } -void Download::sslErrors(const QList & errors) +void Download::sslErrors(const QList& errors) { int i = 1; - for (auto error : errors) - { + for (auto error : errors) { qCritical() << "Download" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString(); auto cert = error.certificate(); qCritical() << "Certificate in question:\n" << cert.toText(); @@ -153,33 +148,27 @@ void Download::sslErrors(const QList & errors) bool Download::handleRedirect() { QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); - if(!redirect.isValid()) - { - if(!m_reply->hasRawHeader("Location")) - { + if (!redirect.isValid()) { + if (!m_reply->hasRawHeader("Location")) { // no redirect -> it's fine to continue return false; } // there is a Location header, but it's not correct. we need to apply some workarounds... QByteArray redirectBA = m_reply->rawHeader("Location"); - if(redirectBA.size() == 0) - { + if (redirectBA.size() == 0) { // empty, yet present redirect header? WTF? return false; } QString redirectStr = QString::fromUtf8(redirectBA); - if(redirectStr.startsWith("//")) - { + if (redirectStr.startsWith("//")) { /* * IF the URL begins with //, we need to insert the URL scheme. * See: https://bugreports.qt.io/browse/QTBUG-41061 * See: http://tools.ietf.org/html/rfc3986#section-4.2 */ redirectStr = m_reply->url().scheme() + ":" + redirectStr; - } - else if(redirectStr.startsWith("/")) - { + } else if (redirectStr.startsWith("/")) { /* * IF the URL begins with /, we need to process it as a relative URL */ @@ -193,16 +182,13 @@ bool Download::handleRedirect() * FIXME: report Qt bug for this */ redirect = QUrl(redirectStr, QUrl::TolerantMode); - if(!redirect.isValid()) - { + if (!redirect.isValid()) { qWarning() << "Failed to parse redirect URL:" << redirectStr; downloadError(QNetworkReply::ProtocolFailure); return false; } qDebug() << "Fixed location header:" << redirect; - } - else - { + } else { qDebug() << "Location header:" << redirect; } @@ -212,35 +198,28 @@ bool Download::handleRedirect() return true; } - void Download::downloadFinished() { // handle HTTP redirection first - if(handleRedirect()) - { + if (handleRedirect()) { qDebug() << "Download redirected:" << m_url.toString(); return; } // if the download failed before this point ... - if (m_status == Job_Failed_Proceed) - { + if (m_status == Job_Failed_Proceed) { qDebug() << "Download failed but we are allowed to proceed:" << m_url.toString(); m_sink->abort(); m_reply.reset(); emit succeeded(m_index_within_job); return; - } - else if (m_status == Job_Failed) - { + } else if (m_status == Job_Failed) { qDebug() << "Download failed in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); emit failed(m_index_within_job); return; - } - else if(m_status == Job_Aborted) - { + } else if (m_status == Job_Aborted) { qDebug() << "Download aborted in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); @@ -250,16 +229,14 @@ void Download::downloadFinished() // make sure we got all the remaining data, if any auto data = m_reply->readAll(); - if(data.size()) - { + if (data.size()) { qDebug() << "Writing extra" << data.size() << "bytes to" << m_target_path; m_status = m_sink->write(data); } // otherwise, finalize the whole graph m_status = m_sink->finalize(*m_reply.get()); - if (m_status != Job_Finished) - { + if (m_status != Job_Finished) { qDebug() << "Download failed to finalize:" << m_url.toString(); m_sink->abort(); m_reply.reset(); @@ -273,32 +250,25 @@ void Download::downloadFinished() void Download::downloadReadyRead() { - if(m_status == Job_InProgress) - { + if (m_status == Job_InProgress) { auto data = m_reply->readAll(); m_status = m_sink->write(data); - if(m_status == Job_Failed) - { + if (m_status == Job_Failed) { qCritical() << "Failed to process response chunk for " << m_target_path; } // qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes"; - } - else - { + } else { qCritical() << "Cannot write to " << m_target_path << ", illegal status" << m_status; } } -} +} // namespace Net bool Net::Download::abort() { - if(m_reply) - { + if (m_reply) { m_reply->abort(); - } - else - { + } else { m_status = Job_Aborted; } return true; diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index e82e1cdb..540ee2fd 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -21,7 +21,8 @@ auto ListModel::debugName() const -> QString void ListModel::fetchMore(const QModelIndex& parent) { - if (parent.isValid()) return; + if (parent.isValid()) + return; if (nextSearchOffset == 0) { qWarning() << "fetchMore with 0 offset is wrong..."; return; @@ -32,7 +33,9 @@ void ListModel::fetchMore(const QModelIndex& parent) auto ListModel::data(const QModelIndex& index, int role) const -> QVariant { int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { + return QString("INVALID INDEX %1").arg(pos); + } ModPlatform::IndexedPack pack = modpacks.at(pos); if (role == Qt::DisplayRole) { @@ -46,7 +49,9 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant } return pack.description; } else if (role == 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); return icon; @@ -63,16 +68,15 @@ void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) { auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); - m_parent->apiProvider()->getVersions(this, - { current.addonId.toString(), getMineVersions(), profile->getModLoader() }); + m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoader() }); } void ListModel::performPaginatedSearch() { auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); - m_parent->apiProvider()->searchMods(this, - { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() }); + m_parent->apiProvider()->searchMods( + this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() }); } void ListModel::refresh() @@ -93,11 +97,9 @@ void ListModel::refresh() void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed) { - if (currentSearchTerm == term - && currentSearchTerm.isNull() == term.isNull() - && currentSort == sort - && !filter_changed) - { return; } + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filter_changed) { + return; + } currentSearchTerm = term; currentSort = sort; @@ -118,7 +120,9 @@ void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallbac void ListModel::requestLogo(QString logo, QString url) { - if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; } + if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { + return; + } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); @@ -129,7 +133,9 @@ void ListModel::requestLogo(QString logo, QString url) QObject::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 (waitingCallbacks.contains(logo)) { + waitingCallbacks.value(logo)(fullPath); + } }); QObject::connect(job, &NetJob::failed, this, [this, logo, job] { @@ -148,7 +154,9 @@ 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) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } + if (modpacks[i].logoName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); + } } } @@ -199,7 +207,9 @@ void ListModel::searchRequestFailed(QString reason) // 409 Gone, notify user to update QMessageBox::critical(nullptr, tr("Error"), //: %1 refers to the launcher itself - QString("%1 %2").arg(m_parent->displayName()).arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME))); + QString("%1 %2") + .arg(m_parent->displayName()) + .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME))); } jobPtr.reset(); @@ -218,9 +228,12 @@ void ListModel::searchRequestFailed(QString reason) void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) { auto& current = m_parent->getCurrent(); - if (addonId != current.addonId) { return; } + if (addonId != current.addonId) { + return; + } + + auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); - QJsonArray arr = doc.array(); try { loadIndexedPackVersions(current, arr); } catch (const JSONValidationError& e) { diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index 905fb2dd..8de2e545 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -1,5 +1,5 @@ #include "FlameModModel.h" - +#include "Json.h" #include "modplatform/flame/FlameModIndex.h" namespace FlameMod { @@ -19,7 +19,7 @@ void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { - return obj.array(); + return Json::ensureArray(obj.object(), "data"); } } // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index fe163cae..f97536e8 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -1,6 +1,6 @@ #include "FlameModel.h" -#include "Application.h" #include +#include "Application.h" #include #include @@ -9,61 +9,46 @@ namespace Flame { -ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) -{ -} +ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} -ListModel::~ListModel() -{ -} +ListModel::~ListModel() {} -int ListModel::rowCount(const QModelIndex &parent) const +int ListModel::rowCount(const QModelIndex& parent) const { return modpacks.size(); } -int ListModel::columnCount(const QModelIndex &parent) const +int ListModel::columnCount(const QModelIndex& parent) const { return 1; } -QVariant ListModel::data(const QModelIndex &index, int role) const +QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } IndexedPack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { + if (role == Qt::DisplayRole) { return pack.name; - } - else if (role == Qt::ToolTipRole) - { - if(pack.description.length() > 100) - { - //some magic to prevent to long tooltips and replace html linebreaks + } else if (role == Qt::ToolTipRole) { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks QString edit = pack.description.left(97); edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); return edit; - } return pack.description; - } - else if(role == Qt::DecorationRole) - { - if(m_logoMap.contains(pack.logoName)) - { + } else if (role == Qt::DecorationRole) { + 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); + ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); return icon; - } - else if(role == Qt::UserRole) - { + } else if (role == Qt::UserRole) { QVariant v; v.setValue(pack); return v; @@ -76,9 +61,9 @@ 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) { - emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + for (int i = 0; i < modpacks.size(); i++) { + if (modpacks[i].logoName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } } } @@ -91,8 +76,7 @@ void ListModel::logoFailed(QString logo) void ListModel::requestLogo(QString logo, QString url) { - if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) - { + if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; } @@ -101,18 +85,15 @@ void ListModel::requestLogo(QString logo, QString url) job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] - { + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { job->deleteLater(); emit logoLoaded(logo, QIcon(fullPath)); - if(waitingCallbacks.contains(logo)) - { + if (waitingCallbacks.contains(logo)) { waitingCallbacks.value(logo)(fullPath); } }); - QObject::connect(job, &NetJob::failed, this, [this, logo, job] - { + QObject::connect(job, &NetJob::failed, this, [this, logo, job] { job->deleteLater(); emit logoFailed(logo); }); @@ -122,19 +103,16 @@ void ListModel::requestLogo(QString logo, QString url) m_loadingLogos.append(logo); } -void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) { - if(m_logoMap.contains(logo)) - { + if (m_logoMap.contains(logo)) { callback(APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); - } - else - { + } else { requestLogo(logo, logoUrl); } } -Qt::ItemFlags ListModel::flags(const QModelIndex &index) const +Qt::ItemFlags ListModel::flags(const QModelIndex& index) const { return QAbstractListModel::flags(index); } @@ -148,7 +126,7 @@ void ListModel::fetchMore(const QModelIndex& parent) { if (parent.isValid()) return; - if(nextSearchOffset == 0) { + if (nextSearchOffset == 0) { qWarning() << "fetchMore with 0 offset is wrong..."; return; } @@ -157,17 +135,20 @@ void ListModel::fetchMore(const QModelIndex& parent) void ListModel::performPaginatedSearch() { - NetJob *netJob = new NetJob("Flame::Search", APPLICATION->network()); + NetJob* netJob = new NetJob("Flame::Search", APPLICATION->network()); auto searchUrl = QString( - "https://addons-ecs.forgesvc.net/api/v2/addon/search?" - "categoryId=0&" - "gameId=432&" - "index=%1&" - "pageSize=25&" - "searchFilter=%2&" - "sectionId=4471&" - "sort=%3" - ).arg(nextSearchOffset).arg(currentSearchTerm).arg(currentSort); + "https://api.curseforge.com/v1/mods/search?" + "gameId=432&" + "classId=4471&" + "index=%1&" + "pageSize=25&" + "searchFilter=%2&" + "sortField=%3&" + "sortOrder=desc") + .arg(nextSearchOffset) + .arg(currentSearchTerm) + .arg(currentSort + 1); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); @@ -177,17 +158,16 @@ void ListModel::performPaginatedSearch() void ListModel::searchWithTerm(const QString& term, int sort) { - if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { return; } currentSearchTerm = term; currentSort = sort; - if(jobPtr) { + if (jobPtr) { jobPtr->abort(); searchState = ResetRequested; return; - } - else { + } else { beginResetModel(); modpacks.clear(); endResetModel(); @@ -203,30 +183,28 @@ void Flame::ListModel::searchRequestFinished() 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(); + 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 = doc.array(); - for(auto packRaw : packs) { + auto packs = Json::ensureArray(doc.object(), "data"); + for (auto packRaw : packs) { auto packObj = packRaw.toObject(); Flame::IndexedPack pack; - try - { + try { Flame::loadIndexedPack(pack, packObj); newList.append(pack); - } - catch(const JSONValidationError &e) - { + } catch (const JSONValidationError& e) { qWarning() << "Error while loading pack from CurseForge: " << e.cause(); continue; } } - if(packs.size() < 25) { + if (packs.size() < 25) { searchState = Finished; } else { nextSearchOffset += 25; @@ -241,7 +219,7 @@ void Flame::ListModel::searchRequestFailed(QString reason) { jobPtr.reset(); - if(searchState == ResetRequested) { + if (searchState == ResetRequested) { beginResetModel(); modpacks.clear(); endResetModel(); @@ -253,5 +231,4 @@ void Flame::ListModel::searchRequestFailed(QString reason) } } -} - +} // namespace Flame diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index c90294ce..ec774621 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -39,13 +39,12 @@ #include #include "Application.h" +#include "FlameModel.h" +#include "InstanceImportTask.h" #include "Json.h" #include "ui/dialogs/NewInstanceDialog.h" -#include "InstanceImportTask.h" -#include "FlameModel.h" -FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget *parent) - : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog) +FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog) { ui->setupUi(this); connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch); @@ -112,10 +111,8 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) { ui->versionSelectionBox->clear(); - if(!first.isValid()) - { - if(isOpened) - { + if (!first.isValid()) { + if (isOpened) { dialog->setSuggestedPack(); } return; @@ -130,14 +127,14 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) else text = "
" + name + ""; if (!current.authors.empty()) { - auto authorToStr = [](Flame::ModpackAuthor & author) { - if(author.url.isEmpty()) { + auto authorToStr = [](Flame::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 : current.authors) { authorStrs.push_back(authorToStr(author)); } text += "
" + tr(" by ") + authorStrs.join(", "); @@ -146,53 +143,46 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) ui->packDescription->setHtml(text + current.description); - if (current.versionsLoaded == false) - { + if (current.versionsLoaded == false) { qDebug() << "Loading flame modpack versions"; auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network()); auto response = new QByteArray(); int addonId = current.addonId; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response)); + netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] - { - if(addonId != current.addonId){ - return; //wrong request + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] { + if (addonId != 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(); + 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; } - QJsonArray arr = doc.array(); - try - { + auto arr = Json::ensureArray(doc.object(), "data"); + try { Flame::loadIndexedPackVersions(current, arr); - } - catch(const JSONValidationError &e) - { + } catch (const JSONValidationError& e) { qDebug() << *response; qWarning() << "Error while reading flame modpack version: " << e.cause(); } - for(auto version : current.versions) { + for (auto version : current.versions) { ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); } suggestCurrent(); }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] - { + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); delete response; }); netJob->start(); - } - else - { - for(auto version : current.versions) { + } else { + for (auto version : current.versions) { ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); } @@ -202,13 +192,11 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) void FlamePage::suggestCurrent() { - if(!isOpened) - { + if (!isOpened) { return; } - if (selectedVersion.isEmpty()) - { + if (selectedVersion.isEmpty()) { dialog->setSuggestedPack(); return; } @@ -216,16 +204,13 @@ void FlamePage::suggestCurrent() dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion)); QString editedLogoName; editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0); - listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) - { - dialog->setSuggestedIconFromFile(logo, editedLogoName); - }); + listModel->getLogo(current.logoName, current.logoUrl, + [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } void FlamePage::onVersionSelectionChanged(QString data) { - if(data.isNull() || data.isEmpty()) - { + if (data.isNull() || data.isEmpty()) { selectedVersion = ""; return; } From bdd2d57808004ccd12b2438c7af9d163fbe96c0d Mon Sep 17 00:00:00 2001 From: Ozynt <58683893+Ozynt@users.noreply.github.com> Date: Sun, 8 May 2022 11:19:53 +0200 Subject: [PATCH 343/605] This makes more sense --- launcher/LaunchController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 4cb62e69..002c08b9 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -93,7 +93,7 @@ void LaunchController::decideAccount() auto reply = CustomMessageBox::selectable( m_parentWidget, tr("No Accounts"), - tr("In order to play Minecraft, you must have at least one Mojang or Minecraft " + tr("In order to play Minecraft, you must have at least one Mojang or Microsoft " "account logged in. " "Would you like to open the account manager to add an account now?"), QMessageBox::Information, From e9b3140d128b698440711d2a99edc268c545a06a Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 8 May 2022 16:25:45 +0200 Subject: [PATCH 344/605] Update launcher/modplatform/flame/FlameModIndex.cpp --- launcher/modplatform/flame/FlameModIndex.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 6334e88c..b23ec7cc 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -48,7 +48,7 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, for (auto mcVer : versionArray) { auto str = mcVer.toString(); - if (str.indexOf('.') > -1) + if (str.contains('.')) file.mcVersion.append(mcVer.toString()); } From c4549a537559d1cd5897b9352c0832e77a48e8f0 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 8 May 2022 16:25:51 +0200 Subject: [PATCH 345/605] Update launcher/modplatform/flame/FlameModIndex.cpp Co-authored-by: flow --- launcher/modplatform/flame/FlameModIndex.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index b23ec7cc..ba0824cf 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -49,7 +49,7 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, auto str = mcVer.toString(); if (str.contains('.')) - file.mcVersion.append(mcVer.toString()); + file.mcVersion.append(str); } file.addonId = pack.addonId; From dd11ccb3fdbfc9555e38bd1d5b673b72ff8d7061 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sun, 8 May 2022 16:30:12 +0200 Subject: [PATCH 346/605] bump to 1.2.2 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a7824a99..1ca8f28f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,7 +69,7 @@ set(Launcher_HELP_URL "https://polymc.org/wiki/help-pages/%1" CACHE STRING "URL ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 1) set(Launcher_VERSION_MINOR 2) -set(Launcher_VERSION_HOTFIX 1) +set(Launcher_VERSION_HOTFIX 2) # Build number set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") From f4237be9bd5a230f0bea8d485805c63eca2e8bce Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 29 Apr 2022 09:10:32 +0200 Subject: [PATCH 347/605] Merge pull request #492 from DioEgizio/appimage-fix --- .github/workflows/build.yml | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6d1189b..57c04e21 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,9 @@ jobs: - os: ubuntu-20.04 + - os: ubuntu-20.04 + appimage: true + - os: windows-2022 name: "Windows-i686" msystem: mingw32 @@ -66,30 +69,25 @@ jobs: ver_short=`git rev-parse --short HEAD` echo "VERSION=$ver_short" >> $GITHUB_ENV - - name: Install OpenJDK - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' - - name: Install Qt (macOS) if: runner.os == 'macOS' run: | brew update - brew install qt@5 + brew install qt@5 ninja + + - name: Update Qt (AppImage) + if: runner.os == 'Linux' && matrix.appimage == true + run: | + sudo add-apt-repository ppa:savoury1/qt-5-15 - name: Install Qt (Linux) if: runner.os == 'Linux' run: | sudo apt-get -y update - sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 - - - name: Install Ninja - if: runner.os != 'Windows' - uses: urkle/action-get-ninja@v1 + sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 ninja-build - name: Prepare AppImage (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage == true run: | wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" wget "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage" @@ -167,7 +165,7 @@ jobs: cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable - name: Package (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage != true run: | cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_DIR }} @@ -175,7 +173,7 @@ jobs: tar --owner root --group root -czf ../PolyMC.tar.gz * - name: Package (Linux, portable) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage != true run: | cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable @@ -184,7 +182,7 @@ jobs: tar -czf ../PolyMC-portable.tar.gz * - name: Package AppImage (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage == true shell: bash run: | cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr @@ -234,21 +232,21 @@ jobs: path: ${{ env.INSTALL_PORTABLE_DIR }}/** - name: Upload binary tarball (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage != true uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC.tar.gz - name: Upload binary tarball (Linux, portable) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage != true uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC-portable.tar.gz - name: Upload AppImage (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage == true uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage From ac66bddeda955db5e81077ddd2116e8ae51edf52 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 25 Apr 2022 21:55:00 +0200 Subject: [PATCH 348/605] Merge pull request #482 from TheCodex6824/mojang-auth-fix --- launcher/CMakeLists.txt | 2 + launcher/minecraft/auth/Parsers.cpp | 175 ++++++++++++++++++ launcher/minecraft/auth/Parsers.h | 1 + launcher/minecraft/auth/Yggdrasil.cpp | 22 +++ launcher/minecraft/auth/flows/Mojang.cpp | 6 +- .../auth/steps/MinecraftProfileStepMojang.cpp | 94 ++++++++++ .../auth/steps/MinecraftProfileStepMojang.h | 22 +++ 7 files changed, 319 insertions(+), 3 deletions(-) create mode 100644 launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp create mode 100644 launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6ed86726..075c183a 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -235,6 +235,8 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/MigrationEligibilityStep.h minecraft/auth/steps/MinecraftProfileStep.cpp minecraft/auth/steps/MinecraftProfileStep.h + minecraft/auth/steps/MinecraftProfileStepMojang.cpp + minecraft/auth/steps/MinecraftProfileStepMojang.h minecraft/auth/steps/MSAStep.cpp minecraft/auth/steps/MSAStep.h minecraft/auth/steps/XboxAuthorizationStep.cpp diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index 2dd36562..47473899 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -1,4 +1,5 @@ #include "Parsers.h" +#include "Json.h" #include #include @@ -212,6 +213,180 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { return true; } +namespace { + // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee) + // they are needed because the session server doesn't return skin urls for default skins + static const QString SKIN_URL_STEVE = "http://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; + static const QString SKIN_URL_ALEX = "http://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; + + bool isDefaultModelSteve(QString uuid) { + // need to calculate *Java* hashCode of UUID + // if number is even, skin/model is steve, otherwise it is alex + + // just in case dashes are in the id + uuid.remove('-'); + + if (uuid.size() != 32) { + return true; + } + + // qulonglong is guaranteed to be 64 bits + // we need to use unsigned numbers to guarantee truncation below + qulonglong most = uuid.left(16).toULongLong(nullptr, 16); + qulonglong least = uuid.right(16).toULongLong(nullptr, 16); + qulonglong xored = most ^ least; + return ((static_cast(xored >> 32)) ^ static_cast(xored)) % 2 == 0; + } +} + +/** +Uses session server for skin/cape lookup instead of profile, +because locked Mojang accounts cannot access profile endpoint +(https://api.minecraftservices.com/minecraft/profile/) + +ref: https://wiki.vg/Mojang_API#UUID_to_Profile_and_Skin.2FCape + +{ + "id": "", + "name": "", + "properties": [ + { + "name": "textures", + "value": "" + } + ] +} + +decoded base64 "value": +{ + "timestamp": , + "profileId": "", + "profileName": "", + "textures": { + "SKIN": { + "url": "" + }, + "CAPE": { + "url": "" + } + } +} +*/ + +bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { + qDebug() << "Parsing Minecraft profile..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = Json::requireObject(doc, "mojang minecraft profile"); + if(!getString(obj.value("id"), output.id)) { + qWarning() << "Minecraft profile id is not a string"; + return false; + } + + if(!getString(obj.value("name"), output.name)) { + qWarning() << "Minecraft profile name is not a string"; + return false; + } + + auto propsArray = obj.value("properties").toArray(); + QByteArray texturePayload; + for( auto p : propsArray) { + auto pObj = p.toObject(); + auto name = pObj.value("name"); + if (!name.isString() || name.toString() != "textures") { + continue; + } + + 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()) { + break; + } + } + + if (texturePayload.isNull()) { + qWarning() << "No texture payload data"; + return false; + } + + doc = QJsonDocument::fromJson(texturePayload, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response as JSON: " << jsonError.errorString(); + return false; + } + + obj = Json::requireObject(doc, "session texture payload"); + auto textures = obj.value("textures"); + if (!textures.isObject()) { + qWarning() << "No textures array in response"; + return false; + } + + Skin skinOut; + // fill in default skin info ourselves, as this endpoint doesn't provide it + bool steve = isDefaultModelSteve(output.id); + skinOut.variant = steve ? "classic" : "slim"; + skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX; + // sadly we can't figure this out, but I don't think it really matters... + skinOut.id = "00000000-0000-0000-0000-000000000000"; + Cape capeOut; + auto tObj = textures.toObject(); + for (auto idx = tObj.constBegin(); idx != tObj.constEnd(); ++idx) { + if (idx->isObject()) { + if (idx.key() == "SKIN") { + auto skin = idx->toObject(); + if (!getString(skin.value("url"), skinOut.url)) { + qWarning() << "Skin url is not a string"; + return false; + } + + auto maybeMeta = skin.find("metadata"); + if (maybeMeta != skin.end() && maybeMeta->isObject()) { + auto meta = maybeMeta->toObject(); + // might not be present + getString(meta.value("model"), skinOut.variant); + } + } + else if (idx.key() == "CAPE") { + auto cape = idx->toObject(); + if (!getString(cape.value("url"), capeOut.url)) { + qWarning() << "Cape url is not a string"; + return false; + } + + // 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 :( + capeOut.alias = "cape"; + } + } + } + + output.skin = skinOut; + if (capeOut.alias == "cape") { + output.capes = QMap({{capeOut.alias, capeOut}}); + output.currentCape = capeOut.alias; + } + + output.validity = Katabasis::Validity::Certain; + return true; +} + bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) { qDebug() << "Parsing Minecraft entitlements..."; #ifndef NDEBUG diff --git a/launcher/minecraft/auth/Parsers.h b/launcher/minecraft/auth/Parsers.h index dac7f69b..2666d890 100644 --- a/launcher/minecraft/auth/Parsers.h +++ b/launcher/minecraft/auth/Parsers.h @@ -14,6 +14,7 @@ namespace Parsers bool parseMojangResponse(QByteArray &data, Katabasis::Token &output); bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output); + bool parseMinecraftProfileMojang(QByteArray &data, MinecraftProfile &output); bool parseMinecraftEntitlements(QByteArray &data, MinecraftEntitlement &output); bool parseRolloutResponse(QByteArray &data, bool& result); } diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp index 7ac842a6..29978411 100644 --- a/launcher/minecraft/auth/Yggdrasil.cpp +++ b/launcher/minecraft/auth/Yggdrasil.cpp @@ -209,6 +209,28 @@ void Yggdrasil::processResponse(QJsonObject responseData) { m_data->yggdrasilToken.validity = Katabasis::Validity::Certain; m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); + // Get UUID here since we need it for later + auto profile = responseData.value("selectedProfile"); + if (!profile.isObject()) { + changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a selected profile.")); + return; + } + + auto profileObj = profile.toObject(); + for (auto i = profileObj.constBegin(); i != profileObj.constEnd(); ++i) { + if (i.key() == "name" && i.value().isString()) { + m_data->minecraftProfile.name = i->toString(); + } + else if (i.key() == "id" && i.value().isString()) { + m_data->minecraftProfile.id = i->toString(); + } + } + + if (m_data->minecraftProfile.id.isEmpty()) { + changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a UUID in selected profile.")); + return; + } + // We've made it through the minefield of possible errors. Return true to indicate that // we've succeeded. qDebug() << "Finished reading authentication response."; diff --git a/launcher/minecraft/auth/flows/Mojang.cpp b/launcher/minecraft/auth/flows/Mojang.cpp index 4661dbe2..b86b0936 100644 --- a/launcher/minecraft/auth/flows/Mojang.cpp +++ b/launcher/minecraft/auth/flows/Mojang.cpp @@ -1,7 +1,7 @@ #include "Mojang.h" #include "minecraft/auth/steps/YggdrasilStep.h" -#include "minecraft/auth/steps/MinecraftProfileStep.h" +#include "minecraft/auth/steps/MinecraftProfileStepMojang.h" #include "minecraft/auth/steps/MigrationEligibilityStep.h" #include "minecraft/auth/steps/GetSkinStep.h" @@ -10,7 +10,7 @@ MojangRefresh::MojangRefresh( QObject *parent ) : AuthFlow(data, parent) { m_steps.append(new YggdrasilStep(m_data, QString())); - m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new MinecraftProfileStepMojang(m_data)); m_steps.append(new MigrationEligibilityStep(m_data)); m_steps.append(new GetSkinStep(m_data)); } @@ -21,7 +21,7 @@ MojangLogin::MojangLogin( QObject *parent ): AuthFlow(data, parent), m_password(password) { m_steps.append(new YggdrasilStep(m_data, m_password)); - m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new MinecraftProfileStepMojang(m_data)); m_steps.append(new MigrationEligibilityStep(m_data)); m_steps.append(new GetSkinStep(m_data)); } diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp new file mode 100644 index 00000000..d3035272 --- /dev/null +++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp @@ -0,0 +1,94 @@ +#include "MinecraftProfileStepMojang.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) { + +} + +MinecraftProfileStepMojang::~MinecraftProfileStepMojang() noexcept = default; + +QString MinecraftProfileStepMojang::describe() { + return tr("Fetching the Minecraft profile."); +} + + +void MinecraftProfileStepMojang::perform() { + if (m_data->minecraftProfile.id.isEmpty()) { + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile.")); + return; + } + + // use session server instead of profile due to profile endpoint being locked for locked Mojang accounts + QUrl url = QUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + m_data->minecraftProfile.id); + QNetworkRequest req = QNetworkRequest(url); + AuthRequest *request = new AuthRequest(this); + connect(request, &AuthRequest::finished, this, &MinecraftProfileStepMojang::onRequestDone); + request->get(req); +} + +void MinecraftProfileStepMojang::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void MinecraftProfileStepMojang::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + if (error == QNetworkReply::ContentNotFoundError) { + // NOTE: Succeed even if we do not have a profile. This is a valid account state. + if(m_data->type == AccountType::Mojang) { + m_data->minecraftEntitlement.canPlayMinecraft = false; + m_data->minecraftEntitlement.ownsMinecraft = false; + } + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_SUCCEEDED, + tr("Account has no Minecraft profile.") + ); + return; + } + if (error != QNetworkReply::NoError) { + qWarning() << "Error getting profile:"; + qWarning() << " HTTP Status: " << requestor->httpStatus_; + qWarning() << " Internal error no.: " << error; + qWarning() << " Error string: " << requestor->errorString_; + + qWarning() << " Response:"; + qWarning() << QString::fromUtf8(data); + + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile acquisition failed.") + ); + return; + } + if(!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) { + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile response could not be parsed") + ); + return; + } + + if(m_data->type == AccountType::Mojang) { + auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain; + m_data->minecraftEntitlement.canPlayMinecraft = validProfile; + m_data->minecraftEntitlement.ownsMinecraft = validProfile; + } + emit finished( + AccountTaskState::STATE_WORKING, + tr("Minecraft Java profile acquisition succeeded.") + ); +} diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h new file mode 100644 index 00000000..e06b30ab --- /dev/null +++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class MinecraftProfileStepMojang : public AuthStep { + Q_OBJECT + +public: + explicit MinecraftProfileStepMojang(AccountData *data); + virtual ~MinecraftProfileStepMojang() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; From cab40026f2a1253539d0d8e363f1aea32b054bd1 Mon Sep 17 00:00:00 2001 From: timoreo22 Date: Mon, 2 May 2022 10:45:58 +0200 Subject: [PATCH 349/605] Merge pull request #475 from Scrumplex/fix-hide-all-tokens Hide all tokens for non-Debug builds for log and logfiles --- launcher/minecraft/auth/steps/LauncherLoginStep.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp index c978bd07..f5697223 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -50,7 +50,9 @@ void LauncherLoginStep::onRequestDone( auto requestor = qobject_cast(QObject::sender()); requestor->deleteLater(); +#ifndef NDEBUG qDebug() << data; +#endif if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; #ifndef NDEBUG From ea9d61c21cd0b1419329371fb22e4c101e67dda0 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 8 May 2022 23:19:23 -0400 Subject: [PATCH 350/605] Retranslate account actions after switching language --- launcher/ui/MainWindow.cpp | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index f34cf1ab..e5c4708c 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -238,6 +238,9 @@ public: TranslatedAction actionREDDIT; TranslatedAction actionAbout; + TranslatedAction actionNoAccountsAdded; + TranslatedAction actionNoDefaultAccount; + QVector all_toolbuttons; QWidget *centralWidget = nullptr; @@ -1252,10 +1255,14 @@ void MainWindow::repopulateAccountsMenu() if (accounts->count() <= 0) { - QAction *action = new QAction(tr("No accounts added!"), this); - action->setEnabled(false); - accountMenu->addAction(action); - ui->profileMenu->addAction(action); + ui->all_actions.removeAll(&ui->actionNoAccountsAdded); + ui->actionNoAccountsAdded = TranslatedAction(this); + ui->actionNoAccountsAdded->setObjectName(QStringLiteral("actionNoAccountsAdded")); + ui->actionNoAccountsAdded.setTextId(QT_TRANSLATE_NOOP("MainWindow", "No accounts added!")); + ui->actionNoAccountsAdded->setEnabled(false); + accountMenu->addAction(ui->actionNoAccountsAdded); + ui->profileMenu->addAction(ui->actionNoAccountsAdded); + ui->all_actions.append(&ui->actionNoAccountsAdded); } else { @@ -1295,18 +1302,23 @@ void MainWindow::repopulateAccountsMenu() accountMenu->addSeparator(); ui->profileMenu->addSeparator(); - QAction *action = new QAction(tr("No Default Account"), this); - action->setCheckable(true); - action->setIcon(APPLICATION->getThemedIcon("noaccount")); - action->setData(-1); - action->setShortcut(QKeySequence(tr("Ctrl+0"))); + ui->all_actions.removeAll(&ui->actionNoDefaultAccount); + ui->actionNoDefaultAccount = TranslatedAction(this); + ui->actionNoDefaultAccount->setObjectName(QStringLiteral("actionNoDefaultAccount")); + ui->actionNoDefaultAccount.setTextId(QT_TRANSLATE_NOOP("MainWindow", "No Default Account")); + ui->actionNoDefaultAccount->setCheckable(true); + ui->actionNoDefaultAccount->setIcon(APPLICATION->getThemedIcon("noaccount")); + ui->actionNoDefaultAccount->setData(-1); + ui->actionNoDefaultAccount->setShortcut(QKeySequence(tr("Ctrl+0"))); if (!defaultAccount) { - action->setChecked(true); + ui->actionNoDefaultAccount->setChecked(true); } - accountMenu->addAction(action); - ui->profileMenu->addAction(action); - connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); + accountMenu->addAction(ui->actionNoDefaultAccount); + ui->profileMenu->addAction(ui->actionNoDefaultAccount); + connect(ui->actionNoDefaultAccount, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); + ui->all_actions.append(&ui->actionNoDefaultAccount); + ui->actionNoDefaultAccount.retranslate(); accountMenu->addSeparator(); ui->profileMenu->addSeparator(); From 5171d99fe5c8d08014412e320679b32c73fd2789 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 8 May 2022 23:42:37 -0400 Subject: [PATCH 351/605] Retranslate playtime text immediately when language is changed --- launcher/ui/MainWindow.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index e5c4708c..85f4157b 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -831,7 +831,7 @@ public: QMetaObject::connectSlotsByName(MainWindow); } // setupUi - void retranslateUi(QMainWindow *MainWindow) + void retranslateUi(MainWindow *MainWindow) { QString winTitle = tr("%1 - Version %2", "Launcher - Version X").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()); MainWindow->setWindowTitle(winTitle); @@ -851,6 +851,12 @@ public: // submenu buttons foldersMenuButton->setText(tr("Folders")); helpMenuButton->setText(tr("Help")); + + // playtime counter + if (MainWindow->m_statusCenter) + { + MainWindow->updateStatusCenter(); + } } // retranslateUi }; From 40e0252d7d5f994b8ff6764bcdb7d9416881ccfe Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 9 May 2022 00:54:47 -0400 Subject: [PATCH 352/605] Show "executable" screenshots in the screenshot manager Since the readable/writable filter was removed to do this, extra code was added to enable/disable certain buttons based on whether the screenshot is readable or writable. --- .../ui/pages/instance/ScreenshotsPage.cpp | 27 ++++++++++++++++++- launcher/ui/pages/instance/ScreenshotsPage.h | 1 + 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index e694ebe3..2cf17b32 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -251,7 +251,7 @@ ScreenshotsPage::ScreenshotsPage(QString path, QWidget *parent) m_model.reset(new QFileSystemModel()); m_filterModel.reset(new FilterModel()); m_filterModel->setSourceModel(m_model.get()); - m_model->setFilter(QDir::Files | QDir::Writable | QDir::Readable); + m_model->setFilter(QDir::Files); m_model->setReadOnly(false); m_model->setNameFilters({"*.png"}); m_model->setNameFilterDisables(false); @@ -343,6 +343,29 @@ void ScreenshotsPage::onItemActivated(QModelIndex index) DesktopServices::openFile(info.absoluteFilePath()); } +void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection &selected) +{ + bool allReadable = !selected.isEmpty(); + bool allWritable = !selected.isEmpty(); + + for (auto index : selected.indexes()) + { + if (!index.isValid()) + break; + auto info = m_model->fileInfo(index); + if (!info.isReadable()) + allReadable = false; + if (!info.isWritable()) + allWritable = false; + } + + ui->actionUpload->setEnabled(allReadable); + ui->actionCopy_Image->setEnabled(allReadable); + ui->actionCopy_File_s->setEnabled(allReadable); + ui->actionDelete->setEnabled(allWritable); + ui->actionRename->setEnabled(allWritable); +} + void ScreenshotsPage::on_actionView_Folder_triggered() { DesktopServices::openDirectory(m_folder, true); @@ -503,6 +526,8 @@ void ScreenshotsPage::openedImpl() if(idx.isValid()) { ui->listView->setModel(m_filterModel.get()); + connect(ui->listView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ScreenshotsPage::onCurrentSelectionChanged); + onCurrentSelectionChanged(ui->listView->selectionModel()->selection()); // set initial button enable states ui->listView->setRootIndex(m_filterModel->mapFromSource(idx)); } else diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index 50cf1a17..c22706af 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -100,6 +100,7 @@ private slots: void on_actionRename_triggered(); void on_actionView_Folder_triggered(); void onItemActivated(QModelIndex); + void onCurrentSelectionChanged(const QItemSelection &selected); void ShowContextMenu(const QPoint &pos); private: From 96b2758169a7a003e2daba150bb897e702316c6f Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Mon, 9 May 2022 17:42:17 +0200 Subject: [PATCH 353/605] fix websiteurl in curseforge modpacks --- launcher/modplatform/flame/FlamePackIndex.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index 549cace6..ac24c647 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -6,7 +6,7 @@ void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); pack.name = Json::requireString(obj, "name"); - pack.websiteUrl = Json::ensureString(obj, "websiteUrl", ""); + pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.description = Json::ensureString(obj, "summary", ""); auto logo = Json::requireObject(obj, "logo"); From 288e7bc9c5e1358d1ad78961cd1a771e6292014e Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 9 May 2022 03:20:53 -0400 Subject: [PATCH 354/605] Make profile menu scrollable --- launcher/ui/MainWindow.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index f34cf1ab..9e1074f8 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -950,6 +950,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow ui->mainToolBar->addWidget(spacer); accountMenu = new QMenu(this); + // Use undocumented property... https://stackoverflow.com/questions/7121718/create-a-scrollbar-in-a-submenu-qt + accountMenu->setStyleSheet("QMenu { menu-scrollable: 1; }"); repopulateAccountsMenu(); From 527fa7ba9cb4e29277acdbedf4880f6fc2255a8b Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 9 May 2022 15:58:08 -0400 Subject: [PATCH 355/605] Hide temporary directory in instances folder --- launcher/InstanceList.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 6e37e3d8..847d897e 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -38,6 +38,10 @@ #include "ExponentialSeries.h" #include "WatchLock.h" +#ifdef Q_OS_WIN32 +#include +#endif + const static int GROUP_FILE_FORMAT_VERSION = 1; InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent) @@ -851,13 +855,18 @@ Task * InstanceList::wrapInstanceTask(InstanceTask * task) QString InstanceList::getStagedInstancePath() { QString key = QUuid::createUuid().toString(); - QString relPath = FS::PathCombine("_LAUNCHER_TEMP/" , key); + QString tempDir = ".LAUNCHER_TEMP/"; + QString relPath = FS::PathCombine(tempDir, key); QDir rootPath(m_instDir); auto path = FS::PathCombine(m_instDir, relPath); if(!rootPath.mkpath(relPath)) { return QString(); } +#ifdef Q_OS_WIN32 + auto tempPath = FS::PathCombine(m_instDir, tempDir); + SetFileAttributesA(tempPath.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); +#endif return path; } From 512d7b07d01d82822a728b40dddb0efbf441dc00 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Wed, 11 May 2022 15:18:07 +0200 Subject: [PATCH 356/605] chore: add version of polymc area in bug report template --- .github/ISSUE_TEMPLATE/bug_report.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1ede3f74..b387f46a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -23,6 +23,13 @@ body: - macOS - Linux - Other +- type: textarea + attributes: + label: Version of PolyMC + description: The version of PolyMC used in the bug report. + placeholder: PolyMC 1.2.2 + validations: + required: true - type: textarea attributes: label: Description of bug From 37e8f495b420be9c59e0349235bb5940249ed666 Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Thu, 12 May 2022 23:39:48 +1000 Subject: [PATCH 357/605] CurseForge API Key update to PolyMC key Use the key CurseForge provided me to use for PolyMC *pr done on mobile if someone could test that would be great* --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b052fa1e..4d3683d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,7 +91,7 @@ set(Launcher_MSA_CLIENT_ID "549033b2-1532-4d4e-ae77-1bbaa46f9d74" CACHE STRING " # CurseForge API Key # CHANGE THIS IF YOU FORK THIS PROJECT! -set(Launcher_CURSEFORGE_API_KEY "$2a$10$iR1RdPDG95FWdILZbHuoMOlV4vL4eckBx7QPZR6SVZmliEb9ZQplu" CACHE STRING "CurseForge API Key") +set(Launcher_CURSEFORGE_API_KEY "$2a$10$1Oqr2MX3O4n/ilhFGc597u8tfI3L2Hyr9/rtWDAMRjghSQV2QUuxq" CACHE STRING "CurseForge API Key") # Bug tracker URL set(Launcher_BUG_TRACKER_URL "https://github.com/PolyMC/PolyMC/issues" CACHE STRING "URL for the bug tracker.") From 046f1e6e58b7a28f336bb2ed79995656fe66f0bf Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Thu, 12 May 2022 17:00:17 -0400 Subject: [PATCH 358/605] Add instance overrides for miscellaneous settings --- launcher/minecraft/MinecraftInstance.cpp | 7 ++++++- launcher/minecraft/launch/LauncherPartLaunch.cpp | 6 ++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 3ba79178..e20dc24c 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -162,6 +162,11 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO m_settings->registerSetting("JoinServerOnLaunch", false); m_settings->registerSetting("JoinServerOnLaunchAddress", ""); + // Miscellaneous + auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false); + m_settings->registerOverride(globalSettings->getSetting("CloseAfterLaunch"), miscellaneousOverride); + m_settings->registerOverride(globalSettings->getSetting("QuitAfterGameStop"), miscellaneousOverride); + m_components.reset(new PackProfile(this)); } @@ -984,7 +989,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt { process->setCensorFilter(createCensorFilterFromSession(session)); } - if(APPLICATION->settings()->get("QuitAfterGameStop").toBool()) + if(m_settings->get("QuitAfterGameStop").toBool()) { auto step = new QuitAfterGameStop(pptr); process->appendStep(step); diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index 173f29b5..d7010355 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -25,7 +25,8 @@ LauncherPartLaunch::LauncherPartLaunch(LaunchTask *parent) : LaunchStep(parent) { - if (APPLICATION->settings()->get("CloseAfterLaunch").toBool()) + auto instance = parent->instance(); + if (instance->settings()->get("CloseAfterLaunch").toBool()) { std::shared_ptr connection{new QMetaObject::Connection}; *connection = connect(&m_process, &LoggedProcess::log, this, [=](QStringList lines, MessageLevel::Enum level) { @@ -168,7 +169,8 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) } case LoggedProcess::Finished: { - if (APPLICATION->settings()->get("CloseAfterLaunch").toBool()) + auto instance = m_parent->instance(); + if (instance->settings()->get("CloseAfterLaunch").toBool()) APPLICATION->showMainWindow(); m_parent->setPid(-1); From 3aea639fe4359259a27e971ae357e308ae50e69d Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Thu, 12 May 2022 17:11:06 -0400 Subject: [PATCH 359/605] Add UI for miscellaneous instance setting overrides --- .../pages/instance/InstanceSettingsPage.cpp | 19 ++++++++++++ .../ui/pages/instance/InstanceSettingsPage.ui | 29 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index a48c4d69..b4562843 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -101,6 +101,20 @@ 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); @@ -247,6 +261,11 @@ void InstanceSettingsPage::applySettings() 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()); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index 5db2d147..cb66b3ce 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -349,6 +349,35 @@
+ + + + Miscellaneous + + + true + + + false + + + + + + Close the launcher after game window opens + + + + + + + Quit the launcher after game window closes + + + + + + From 8c8eabf7ac1920b47792b26790f3646cb6693ec0 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 21 Apr 2022 22:12:14 -0300 Subject: [PATCH 360/605] refactor: organize a little more the code in launcher/net/ This also reduces some code duplication by using some Task logic in NetAction. --- launcher/InstanceImportTask.cpp | 14 +- launcher/minecraft/AssetsUtils.cpp | 2 +- launcher/net/ByteArraySink.h | 67 +++-- launcher/net/Download.cpp | 60 ++--- launcher/net/Download.h | 14 +- launcher/net/FileSink.cpp | 50 ++-- launcher/net/FileSink.h | 36 +-- launcher/net/MetaCacheSink.cpp | 22 +- launcher/net/MetaCacheSink.h | 27 +- launcher/net/NetAction.h | 127 ++++----- launcher/net/NetJob.cpp | 274 ++++++++++---------- launcher/net/NetJob.h | 109 ++++---- launcher/net/Sink.h | 54 ++-- launcher/screenshots/ImgurAlbumCreation.cpp | 15 +- launcher/screenshots/ImgurAlbumCreation.h | 12 +- launcher/screenshots/ImgurUpload.cpp | 13 +- launcher/screenshots/ImgurUpload.h | 2 +- launcher/tasks/Task.h | 4 +- launcher/translations/TranslationsModel.cpp | 2 +- 19 files changed, 435 insertions(+), 469 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 1a13c997..fc3432c1 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -14,27 +14,25 @@ */ #include "InstanceImportTask.h" +#include +#include "Application.h" #include "BaseInstance.h" #include "FileSystem.h" -#include "Application.h" #include "MMCZip.h" #include "NullInstance.h" -#include "settings/INISettingsObject.h" +#include "icons/IconList.h" #include "icons/IconUtils.h" -#include +#include "settings/INISettingsObject.h" // FIXME: this does not belong here, it's Minecraft/Flame specific +#include +#include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/PackManifest.h" -#include "Json.h" -#include #include "modplatform/technic/TechnicPackProcessor.h" -#include "icons/IconList.h" -#include "Application.h" - InstanceImportTask::InstanceImportTask(const QUrl sourceUrl) { m_sourceUrl = sourceUrl; diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 7290aeb4..281f730f 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -297,7 +297,7 @@ NetAction::Ptr AssetObject::getDownloadAction() auto rawHash = QByteArray::fromHex(hash.toLatin1()); objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); } - objectDL->m_total_progress = size; + objectDL->setProgress(objectDL->getProgress(), size); return objectDL; } return nullptr; diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index 20e6764c..75a66574 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -3,60 +3,59 @@ #include "Sink.h" namespace Net { + /* * Sink object for downloads that uses an external QByteArray it doesn't own as a target. */ -class ByteArraySink : public Sink -{ -public: - ByteArraySink(QByteArray *output) - :m_output(output) - { - // nil - }; +class ByteArraySink : public Sink { + public: + ByteArraySink(QByteArray* output) : m_output(output){}; - virtual ~ByteArraySink() - { - // nil - } + virtual ~ByteArraySink() = default; -public: - JobStatus init(QNetworkRequest & request) override + public: + auto init(QNetworkRequest& request) -> Task::State override { + if(!m_output) + return Task::State::Failed; + m_output->clear(); - if(initAllValidators(request)) - return Job_InProgress; - return Job_Failed; + if (initAllValidators(request)) + return Task::State::Running; + return Task::State::Failed; }; - JobStatus write(QByteArray & data) override + auto write(QByteArray& data) -> Task::State override { + if(!m_output) + return Task::State::Failed; + m_output->append(data); - if(writeAllValidators(data)) - return Job_InProgress; - return Job_Failed; + if (writeAllValidators(data)) + return Task::State::Running; + return Task::State::Failed; } - JobStatus abort() override + auto abort() -> Task::State override { + if(!m_output) + return Task::State::Failed; + m_output->clear(); failAllValidators(); - return Job_Failed; + return Task::State::Failed; } - JobStatus finalize(QNetworkReply &reply) override + auto finalize(QNetworkReply& reply) -> Task::State override { - if(finalizeAllValidators(reply)) - return Job_Finished; - return Job_Failed; + if (finalizeAllValidators(reply)) + return Task::State::Succeeded; + return Task::State::Failed; } - bool hasLocalData() override - { - return false; - } + auto hasLocalData() -> bool override { return false; } -private: - QByteArray * m_output; + private: + QByteArray* m_output; }; -} +} // namespace Net diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 65cc8f67..5b5a04db 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -30,7 +30,7 @@ namespace Net { Download::Download() : NetAction() { - m_status = Job_NotStarted; + m_state = State::Inactive; } Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) @@ -68,29 +68,29 @@ void Download::addValidator(Validator* v) m_sink->addValidator(v); } -void Download::startImpl() +void Download::executeTask() { - if (m_status == Job_Aborted) { + if (getState() == Task::State::AbortedByUser) { qWarning() << "Attempt to start an aborted Download:" << m_url.toString(); emit aborted(m_index_within_job); return; } + QNetworkRequest request(m_url); - m_status = m_sink->init(request); - switch (m_status) { - case Job_Finished: + m_state = m_sink->init(request); + switch (m_state) { + case State::Succeeded: emit succeeded(m_index_within_job); qDebug() << "Download cache hit " << m_url.toString(); return; - case Job_InProgress: + case State::Running: qDebug() << "Downloading " << m_url.toString(); break; - case Job_Failed_Proceed: // this is meaningless in this context. We do need a sink. - case Job_NotStarted: - case Job_Failed: + case State::Inactive: + case State::Failed: emit failed(m_index_within_job); return; - case Job_Aborted: + case State::AbortedByUser: return; } @@ -111,8 +111,7 @@ void Download::startImpl() void Download::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - m_total_progress = bytesTotal; - m_progress = bytesReceived; + setProgress(bytesReceived, bytesTotal); emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); } @@ -120,17 +119,17 @@ void Download::downloadError(QNetworkReply::NetworkError error) { if (error == QNetworkReply::OperationCanceledError) { qCritical() << "Aborted " << m_url.toString(); - m_status = Job_Aborted; + m_state = State::AbortedByUser; } else { if (m_options & Option::AcceptLocalFiles) { if (m_sink->hasLocalData()) { - m_status = Job_Failed_Proceed; + m_state = State::Succeeded; return; } } // error happened during download. qCritical() << "Failed " << m_url.toString() << " with reason " << error; - m_status = Job_Failed; + m_state = State::Failed; } } @@ -194,7 +193,8 @@ bool Download::handleRedirect() m_url = QUrl(redirect.toString()); qDebug() << "Following redirect to " << m_url.toString(); - start(m_network); + startAction(m_network); + return true; } @@ -207,19 +207,20 @@ void Download::downloadFinished() } // if the download failed before this point ... - if (m_status == Job_Failed_Proceed) { + if (m_state == State::Succeeded) // pretend to succeed so we continue processing :) + { qDebug() << "Download failed but we are allowed to proceed:" << m_url.toString(); m_sink->abort(); m_reply.reset(); emit succeeded(m_index_within_job); return; - } else if (m_status == Job_Failed) { + } else if (m_state == State::Failed) { qDebug() << "Download failed in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); emit failed(m_index_within_job); return; - } else if (m_status == Job_Aborted) { + } else if (m_state == State::AbortedByUser) { qDebug() << "Download aborted in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); @@ -231,12 +232,12 @@ void Download::downloadFinished() auto data = m_reply->readAll(); if (data.size()) { qDebug() << "Writing extra" << data.size() << "bytes to" << m_target_path; - m_status = m_sink->write(data); + m_state = m_sink->write(data); } // otherwise, finalize the whole graph - m_status = m_sink->finalize(*m_reply.get()); - if (m_status != Job_Finished) { + m_state = m_sink->finalize(*m_reply.get()); + if (m_state != State::Succeeded) { qDebug() << "Download failed to finalize:" << m_url.toString(); m_sink->abort(); m_reply.reset(); @@ -250,10 +251,10 @@ void Download::downloadFinished() void Download::downloadReadyRead() { - if (m_status == Job_InProgress) { + if (m_state == State::Running) { auto data = m_reply->readAll(); - m_status = m_sink->write(data); - if (m_status == Job_Failed) { + m_state = m_sink->write(data); + if (m_state == State::Failed) { qCritical() << "Failed to process response chunk for " << m_target_path; } // qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes"; @@ -269,12 +270,7 @@ bool Net::Download::abort() if (m_reply) { m_reply->abort(); } else { - m_status = Job_Aborted; + m_state = State::AbortedByUser; } return true; } - -bool Net::Download::canAbort() -{ - return true; -} diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 0f9bfe7f..231ad6a7 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -27,7 +27,7 @@ class Download : public NetAction { Q_OBJECT -public: /* types */ +public: typedef shared_qobject_ptr Ptr; enum class Option { @@ -36,7 +36,7 @@ public: /* types */ }; Q_DECLARE_FLAGS(Options, Option) -protected: /* con/des */ +protected: explicit Download(); public: virtual ~Download(){}; @@ -44,16 +44,16 @@ public: static Download::Ptr makeByteArray(QUrl url, QByteArray *output, Options options = Option::NoOptions); static Download::Ptr makeFile(QUrl url, QString path, Options options = Option::NoOptions); -public: /* methods */ +public: QString getTargetFilepath() { return m_target_path; } void addValidator(Validator * v); bool abort() override; - bool canAbort() override; + bool canAbort() const override { return true; }; -private: /* methods */ +private: bool handleRedirect(); protected slots: @@ -64,9 +64,9 @@ protected slots: void downloadReadyRead() override; public slots: - void startImpl() override; + void executeTask() override; -private: /* data */ +private: // FIXME: remove this, it has no business being here. QString m_target_path; std::unique_ptr m_sink; diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp index 7e9b8929..0d8b09bb 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -1,25 +1,15 @@ #include "FileSink.h" + #include -#include + #include "FileSystem.h" namespace Net { -FileSink::FileSink(QString filename) - :m_filename(filename) -{ - // nil -} - -FileSink::~FileSink() -{ - // nil -} - -JobStatus FileSink::init(QNetworkRequest& request) +Task::State FileSink::init(QNetworkRequest& request) { auto result = initCache(request); - if(result != Job_InProgress) + if(result != Task::State::Running) { return result; } @@ -27,27 +17,27 @@ JobStatus FileSink::init(QNetworkRequest& request) if (!FS::ensureFilePathExists(m_filename)) { qCritical() << "Could not create folder for " + m_filename; - return Job_Failed; + return Task::State::Failed; } wroteAnyData = false; m_output_file.reset(new QSaveFile(m_filename)); if (!m_output_file->open(QIODevice::WriteOnly)) { qCritical() << "Could not open " + m_filename + " for writing"; - return Job_Failed; + return Task::State::Failed; } if(initAllValidators(request)) - return Job_InProgress; - return Job_Failed; + return Task::State::Running; + return Task::State::Failed; } -JobStatus FileSink::initCache(QNetworkRequest &) +Task::State FileSink::initCache(QNetworkRequest &) { - return Job_InProgress; + return Task::State::Running; } -JobStatus FileSink::write(QByteArray& data) +Task::State FileSink::write(QByteArray& data) { if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) { @@ -55,20 +45,20 @@ JobStatus FileSink::write(QByteArray& data) m_output_file->cancelWriting(); m_output_file.reset(); wroteAnyData = false; - return Job_Failed; + return Task::State::Failed; } wroteAnyData = true; - return Job_InProgress; + return Task::State::Running; } -JobStatus FileSink::abort() +Task::State FileSink::abort() { m_output_file->cancelWriting(); failAllValidators(); - return Job_Failed; + return Task::State::Failed; } -JobStatus FileSink::finalize(QNetworkReply& reply) +Task::State FileSink::finalize(QNetworkReply& reply) { bool gotFile = false; QVariant statusCodeV = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute); @@ -86,13 +76,13 @@ JobStatus FileSink::finalize(QNetworkReply& reply) // ask validators for data consistency // we only do this for actual downloads, not 'your data is still the same' cache hits if(!finalizeAllValidators(reply)) - return Job_Failed; + return Task::State::Failed; // nothing went wrong... if (!m_output_file->commit()) { qCritical() << "Failed to commit changes to " << m_filename; m_output_file->cancelWriting(); - return Job_Failed; + return Task::State::Failed; } } // then get rid of the save file @@ -101,9 +91,9 @@ JobStatus FileSink::finalize(QNetworkReply& reply) return finalizeCache(reply); } -JobStatus FileSink::finalizeCache(QNetworkReply &) +Task::State FileSink::finalizeCache(QNetworkReply &) { - return Job_Finished; + return Task::State::Succeeded; } bool FileSink::hasLocalData() diff --git a/launcher/net/FileSink.h b/launcher/net/FileSink.h index 875fe511..9d77b3d0 100644 --- a/launcher/net/FileSink.h +++ b/launcher/net/FileSink.h @@ -1,28 +1,30 @@ #pragma once -#include "Sink.h" + #include +#include "Sink.h" + namespace Net { -class FileSink : public Sink -{ -public: /* con/des */ - FileSink(QString filename); - virtual ~FileSink(); +class FileSink : public Sink { + public: + FileSink(QString filename) : m_filename(filename){}; + virtual ~FileSink() = default; -public: /* methods */ - JobStatus init(QNetworkRequest & request) override; - JobStatus write(QByteArray & data) override; - JobStatus abort() override; - JobStatus finalize(QNetworkReply & reply) override; - bool hasLocalData() override; + public: + auto init(QNetworkRequest& request) -> Task::State override; + auto write(QByteArray& data) -> Task::State override; + auto abort() -> Task::State override; + auto finalize(QNetworkReply& reply) -> Task::State override; -protected: /* methods */ - virtual JobStatus initCache(QNetworkRequest &); - virtual JobStatus finalizeCache(QNetworkReply &reply); + auto hasLocalData() -> bool override; -protected: /* data */ + protected: + virtual auto initCache(QNetworkRequest&) -> Task::State; + virtual auto finalizeCache(QNetworkReply& reply) -> Task::State; + + protected: QString m_filename; bool wroteAnyData = false; std::unique_ptr m_output_file; }; -} +} // namespace Net diff --git a/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp index 5cdf0460..34ba9f56 100644 --- a/launcher/net/MetaCacheSink.cpp +++ b/launcher/net/MetaCacheSink.cpp @@ -12,17 +12,13 @@ MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum) addValidator(md5sum); } -MetaCacheSink::~MetaCacheSink() -{ - // nil -} - -JobStatus MetaCacheSink::initCache(QNetworkRequest& request) +Task::State MetaCacheSink::initCache(QNetworkRequest& request) { if (!m_entry->isStale()) { - return Job_Finished; + return Task::State::Succeeded; } + // check if file exists, if it does, use its information for the request QFile current(m_filename); if(current.exists() && current.size() != 0) @@ -36,25 +32,31 @@ JobStatus MetaCacheSink::initCache(QNetworkRequest& request) request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->getETag().toLatin1()); } } - return Job_InProgress; + + return Task::State::Running; } -JobStatus MetaCacheSink::finalizeCache(QNetworkReply & reply) +Task::State MetaCacheSink::finalizeCache(QNetworkReply & reply) { QFileInfo output_file_info(m_filename); + if(wroteAnyData) { m_entry->setMD5Sum(m_md5Node->hash().toHex().constData()); } + m_entry->setETag(reply.rawHeader("ETag").constData()); + if (reply.hasRawHeader("Last-Modified")) { m_entry->setRemoteChangedTimestamp(reply.rawHeader("Last-Modified").constData()); } + m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch()); m_entry->setStale(false); APPLICATION->metacache()->updateEntry(m_entry); - return Job_Finished; + + return Task::State::Succeeded; } bool MetaCacheSink::hasLocalData() diff --git a/launcher/net/MetaCacheSink.h b/launcher/net/MetaCacheSink.h index edcf7ad1..431e10a8 100644 --- a/launcher/net/MetaCacheSink.h +++ b/launcher/net/MetaCacheSink.h @@ -1,22 +1,23 @@ #pragma once -#include "FileSink.h" + #include "ChecksumValidator.h" +#include "FileSink.h" #include "net/HttpMetaCache.h" namespace Net { -class MetaCacheSink : public FileSink -{ -public: /* con/des */ - MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum); - virtual ~MetaCacheSink(); - bool hasLocalData() override; +class MetaCacheSink : public FileSink { + public: + MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum); + virtual ~MetaCacheSink() = default; -protected: /* methods */ - JobStatus initCache(QNetworkRequest & request) override; - JobStatus finalizeCache(QNetworkReply & reply) override; + auto hasLocalData() -> bool override; -private: /* data */ + protected: + auto initCache(QNetworkRequest& request) -> Task::State override; + auto finalizeCache(QNetworkReply& reply) -> Task::State override; + + private: MetaEntryPtr m_entry; - ChecksumValidator * m_md5Node; + ChecksumValidator* m_md5Node; }; -} +} // namespace Net diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h index efb20953..e15716f6 100644 --- a/launcher/net/NetAction.h +++ b/launcher/net/NetAction.h @@ -1,108 +1,81 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 -enum JobStatus -{ - Job_NotStarted, - Job_InProgress, - Job_Finished, - Job_Failed, - Job_Aborted, - /* - * FIXME: @NUKE this confuses the task failing with us having a fallback in the form of local data. Clear up the confusion. - * Same could be true for aborted task - the presence of pre-existing result is a separate concern - */ - Job_Failed_Proceed -}; +#include "QObjectPtr.h" +#include "tasks/Task.h" -class NetAction : public QObject -{ +class NetAction : public Task { Q_OBJECT -protected: - explicit NetAction() : QObject(nullptr) {}; + protected: + explicit NetAction() : Task(nullptr) {}; -public: + public: using Ptr = shared_qobject_ptr; - virtual ~NetAction() {}; + virtual ~NetAction() = default; - bool isRunning() const - { - return m_status == Job_InProgress; - } - bool isFinished() const - { - return m_status >= Job_Finished; - } - bool wasSuccessful() const - { - return m_status == Job_Finished || m_status == Job_Failed_Proceed; - } + QUrl url() { return m_url; } - qint64 totalProgress() const - { - return m_total_progress; - } - qint64 currentProgress() const - { - return m_progress; - } - virtual bool abort() - { - return false; - } - virtual bool canAbort() - { - return false; - } - QUrl url() - { - return m_url; - } - -signals: + signals: void started(int index); void netActionProgress(int index, qint64 current, qint64 total); void succeeded(int index); void failed(int index); void aborted(int index); -protected slots: + protected slots: virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0; virtual void downloadError(QNetworkReply::NetworkError error) = 0; virtual void downloadFinished() = 0; virtual void downloadReadyRead() = 0; -public slots: - void start(shared_qobject_ptr network) { + public slots: + void startAction(shared_qobject_ptr network) + { m_network = network; - startImpl(); + executeTask(); } -protected: - virtual void startImpl() = 0; + protected: + void executeTask() override {}; -public: + public: shared_qobject_ptr m_network; /// index within the parent job, FIXME: nuke @@ -113,10 +86,4 @@ public: /// source URL QUrl m_url; - - qint64 m_progress = 0; - qint64 m_total_progress = 1; - -protected: - JobStatus m_status = Job_NotStarted; }; diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index 9bad89ed..d08d6c4d 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -1,79 +1,170 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "NetJob.h" #include "Download.h" -#include +auto NetJob::addNetAction(NetAction::Ptr action) -> bool +{ + action->m_index_within_job = m_downloads.size(); + m_downloads.append(action); + part_info pi; + m_parts_progress.append(pi); + + partProgress(m_parts_progress.count() - 1, action->getProgress(), action->getTotalProgress()); + + if (action->isRunning()) { + connect(action.get(), &NetAction::succeeded, this, &NetJob::partSucceeded); + connect(action.get(), &NetAction::failed, this, &NetJob::partFailed); + connect(action.get(), &NetAction::netActionProgress, this, &NetJob::partProgress); + } else { + m_todo.append(m_parts_progress.size() - 1); + } + + return true; +} + +auto NetJob::canAbort() const -> bool +{ + bool canFullyAbort = true; + + // can abort the downloads on the queue? + for (auto index : m_todo) { + auto part = m_downloads[index]; + canFullyAbort &= part->canAbort(); + } + // can abort the active downloads? + for (auto index : m_doing) { + auto part = m_downloads[index]; + canFullyAbort &= part->canAbort(); + } + + return canFullyAbort; +} + +void NetJob::executeTask() +{ + // hack that delays early failures so they can be caught easier + QMetaObject::invokeMethod(this, "startMoreParts", Qt::QueuedConnection); +} + +auto NetJob::getFailedFiles() -> QStringList +{ + QStringList failed; + for (auto index : m_failed) { + failed.push_back(m_downloads[index]->url().toString()); + } + failed.sort(); + return failed; +} + +auto NetJob::abort() -> bool +{ + bool fullyAborted = true; + + // fail all downloads on the queue + m_failed.unite(m_todo.toSet()); + m_todo.clear(); + + // abort active downloads + auto toKill = m_doing.toList(); + for (auto index : toKill) { + auto part = m_downloads[index]; + fullyAborted &= part->abort(); + } + + return fullyAborted; +} void NetJob::partSucceeded(int index) { // do progress. all slots are 1 in size at least - auto &slot = parts_progress[index]; + auto& slot = m_parts_progress[index]; partProgress(index, slot.total_progress, slot.total_progress); m_doing.remove(index); m_done.insert(index); - downloads[index].get()->disconnect(this); + m_downloads[index].get()->disconnect(this); + startMoreParts(); } void NetJob::partFailed(int index) { m_doing.remove(index); - auto &slot = parts_progress[index]; - if (slot.failures == 3) - { + + auto& slot = m_parts_progress[index]; + // Can try 3 times before failing by definitive + if (slot.failures == 3) { m_failed.insert(index); - } - else - { + } else { slot.failures++; m_todo.enqueue(index); } - downloads[index].get()->disconnect(this); + + m_downloads[index].get()->disconnect(this); + startMoreParts(); } void NetJob::partAborted(int index) { m_aborted = true; + m_doing.remove(index); m_failed.insert(index); - downloads[index].get()->disconnect(this); + m_downloads[index].get()->disconnect(this); + startMoreParts(); } void NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal) { - auto &slot = parts_progress[index]; + auto& slot = m_parts_progress[index]; slot.current_progress = bytesReceived; slot.total_progress = bytesTotal; int done = m_done.size(); int doing = m_doing.size(); - int all = parts_progress.size(); + int all = m_parts_progress.size(); qint64 bytesAll = 0; qint64 bytesTotalAll = 0; - for(auto & partIdx: m_doing) - { - auto part = parts_progress[partIdx]; + for (auto& partIdx : m_doing) { + auto part = m_parts_progress[partIdx]; // do not count parts with unknown/nonsensical total size - if(part.total_progress <= 0) - { + if (part.total_progress <= 0) { continue; } bytesAll += part.current_progress; @@ -85,134 +176,53 @@ void NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal) auto current_total = all * 1000; // HACK: make sure it never jumps backwards. // FAIL: This breaks if the size is not known (or is it something else?) and jumps to 1000, so if it is 1000 reset it to inprogress - if(m_current_progress == 1000) { + if (m_current_progress == 1000) { m_current_progress = inprogress; } - if(m_current_progress > current) - { + if (m_current_progress > current) { current = m_current_progress; } m_current_progress = current; setProgress(current, current_total); } -void NetJob::executeTask() -{ - // hack that delays early failures so they can be caught easier - QMetaObject::invokeMethod(this, "startMoreParts", Qt::QueuedConnection); -} - void NetJob::startMoreParts() { - if(!isRunning()) - { - // this actually makes sense. You can put running downloads into a NetJob and then not start it until much later. + if (!isRunning()) { + // this actually makes sense. You can put running m_downloads into a NetJob and then not start it until much later. return; } + // OK. We are actively processing tasks, proceed. // Check for final conditions if there's nothing in the queue. - if(!m_todo.size()) - { - if(!m_doing.size()) - { - if(!m_failed.size()) - { + if (!m_todo.size()) { + if (!m_doing.size()) { + if (!m_failed.size()) { emitSucceeded(); - } - else if(m_aborted) - { + } else if (m_aborted) { emitAborted(); - } - else - { + } else { emitFailed(tr("Job '%1' failed to process:\n%2").arg(objectName()).arg(getFailedFiles().join("\n"))); } } return; } - // There's work to do, try to start more parts. - while (m_doing.size() < 6) - { - if(!m_todo.size()) + + // There's work to do, try to start more parts, to a maximum of 6 concurrent ones. + while (m_doing.size() < 6) { + if (m_todo.size() == 0) return; int doThis = m_todo.dequeue(); m_doing.insert(doThis); - auto part = downloads[doThis]; + + auto part = m_downloads[doThis]; + // connect signals :D - connect(part.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int))); - connect(part.get(), SIGNAL(failed(int)), SLOT(partFailed(int))); - connect(part.get(), SIGNAL(aborted(int)), SLOT(partAborted(int))); - connect(part.get(), SIGNAL(netActionProgress(int, qint64, qint64)), - SLOT(partProgress(int, qint64, qint64))); - part->start(m_network); + connect(part.get(), &NetAction::succeeded, this, &NetJob::partSucceeded); + connect(part.get(), &NetAction::failed, this, &NetJob::partFailed); + connect(part.get(), &NetAction::aborted, this, &NetJob::partAborted); + connect(part.get(), &NetAction::netActionProgress, this, &NetJob::partProgress); + + part->startAction(m_network); } } - - -QStringList NetJob::getFailedFiles() -{ - QStringList failed; - for (auto index: m_failed) - { - failed.push_back(downloads[index]->url().toString()); - } - failed.sort(); - return failed; -} - -bool NetJob::canAbort() const -{ - bool canFullyAbort = true; - // can abort the waiting? - for(auto index: m_todo) - { - auto part = downloads[index]; - canFullyAbort &= part->canAbort(); - } - // can abort the active? - for(auto index: m_doing) - { - auto part = downloads[index]; - canFullyAbort &= part->canAbort(); - } - return canFullyAbort; -} - -bool NetJob::abort() -{ - bool fullyAborted = true; - // fail all waiting - m_failed.unite(m_todo.toSet()); - m_todo.clear(); - // abort active - auto toKill = m_doing.toList(); - for(auto index: toKill) - { - auto part = downloads[index]; - fullyAborted &= part->abort(); - } - return fullyAborted; -} - -bool NetJob::addNetAction(NetAction::Ptr action) -{ - action->m_index_within_job = downloads.size(); - downloads.append(action); - part_info pi; - parts_progress.append(pi); - partProgress(parts_progress.count() - 1, action->currentProgress(), action->totalProgress()); - - if(action->isRunning()) - { - connect(action.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int))); - connect(action.get(), SIGNAL(failed(int)), SLOT(partFailed(int))); - connect(action.get(), SIGNAL(netActionProgress(int, qint64, qint64)), SLOT(partProgress(int, qint64, qint64))); - } - else - { - m_todo.append(parts_progress.size() - 1); - } - return true; -} - -NetJob::~NetJob() = default; diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h index fdea710f..c397e2a1 100644 --- a/launcher/net/NetJob.h +++ b/launcher/net/NetJob.h @@ -1,88 +1,97 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "NetAction.h" -#include "Download.h" -#include "HttpMetaCache.h" #include "tasks/Task.h" -#include "QObjectPtr.h" -class NetJob; +// Those are included so that they are also included by anyone using NetJob +#include "net/Download.h" +#include "net/HttpMetaCache.h" -class NetJob : public Task -{ +class NetJob : public Task { Q_OBJECT -public: + + public: using Ptr = shared_qobject_ptr; explicit NetJob(QString job_name, shared_qobject_ptr network) : Task(), m_network(network) { setObjectName(job_name); } - virtual ~NetJob(); + virtual ~NetJob() = default; - bool addNetAction(NetAction::Ptr action); + void executeTask() override; - NetAction::Ptr operator[](int index) - { - return downloads[index]; - } - const NetAction::Ptr at(const int index) - { - return downloads.at(index); - } - NetAction::Ptr first() - { - if (downloads.size()) - return downloads[0]; - return NetAction::Ptr(); - } - int size() const - { - return downloads.size(); - } - QStringList getFailedFiles(); + auto canAbort() const -> bool override; - bool canAbort() const override; + auto addNetAction(NetAction::Ptr action) -> bool; -private slots: + auto operator[](int index) -> NetAction::Ptr { return m_downloads[index]; } + auto at(int index) -> const NetAction::Ptr { return m_downloads.at(index); } + auto size() const -> int { return m_downloads.size(); } + auto first() -> NetAction::Ptr { return m_downloads.size() != 0 ? m_downloads[0] : NetAction::Ptr{}; } + + auto getFailedFiles() -> QStringList; + + public slots: + // Qt can't handle auto at the start for some reason? + bool abort() override; + + private slots: void startMoreParts(); -public slots: - virtual void executeTask() override; - virtual bool abort() override; - -private slots: void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal); void partSucceeded(int index); void partFailed(int index); void partAborted(int index); -private: + private: shared_qobject_ptr m_network; - struct part_info - { + struct part_info { qint64 current_progress = 0; qint64 total_progress = 1; int failures = 0; }; - QList downloads; - QList parts_progress; + + QList m_downloads; + QList m_parts_progress; QQueue m_todo; QSet m_doing; QSet m_done; diff --git a/launcher/net/Sink.h b/launcher/net/Sink.h index d367fb15..3b2a7f8d 100644 --- a/launcher/net/Sink.h +++ b/launcher/net/Sink.h @@ -5,33 +5,30 @@ #include "Validator.h" namespace Net { -class Sink -{ -public: /* con/des */ - Sink() {}; - virtual ~Sink() {}; +class Sink { + public: + Sink() = default; + virtual ~Sink(){}; -public: /* methods */ - virtual JobStatus init(QNetworkRequest & request) = 0; - virtual JobStatus write(QByteArray & data) = 0; - virtual JobStatus abort() = 0; - virtual JobStatus finalize(QNetworkReply & reply) = 0; + public: + virtual Task::State init(QNetworkRequest& request) = 0; + virtual Task::State write(QByteArray& data) = 0; + virtual Task::State abort() = 0; + virtual Task::State finalize(QNetworkReply& reply) = 0; virtual bool hasLocalData() = 0; - void addValidator(Validator * validator) + void addValidator(Validator* validator) { - if(validator) - { + if (validator) { validators.push_back(std::shared_ptr(validator)); } } -protected: /* methods */ - bool finalizeAllValidators(QNetworkReply & reply) + protected: /* methods */ + bool finalizeAllValidators(QNetworkReply& reply) { - for(auto & validator: validators) - { - if(!validator->validate(reply)) + for (auto& validator : validators) { + if (!validator->validate(reply)) return false; } return true; @@ -39,32 +36,29 @@ protected: /* methods */ bool failAllValidators() { bool success = true; - for(auto & validator: validators) - { + for (auto& validator : validators) { success &= validator->abort(); } return success; } - bool initAllValidators(QNetworkRequest & request) + bool initAllValidators(QNetworkRequest& request) { - for(auto & validator: validators) - { - if(!validator->init(request)) + for (auto& validator : validators) { + if (!validator->init(request)) return false; } return true; } - bool writeAllValidators(QByteArray & data) + bool writeAllValidators(QByteArray& data) { - for(auto & validator: validators) - { - if(!validator->write(data)) + for (auto& validator : validators) { + if (!validator->write(data)) return false; } return true; } -protected: /* data */ + protected: /* data */ std::vector> validators; }; -} +} // namespace Net diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index d5de302a..81fac929 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -13,12 +13,12 @@ ImgurAlbumCreation::ImgurAlbumCreation(QList screenshots) : NetAction(), m_screenshots(screenshots) { m_url = BuildConfig.IMGUR_BASE_URL + "album.json"; - m_status = Job_NotStarted; + m_state = State::Inactive; } -void ImgurAlbumCreation::startImpl() +void ImgurAlbumCreation::executeTask() { - m_status = Job_InProgress; + m_state = State::Running; QNetworkRequest request(m_url); request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); @@ -43,11 +43,11 @@ void ImgurAlbumCreation::startImpl() void ImgurAlbumCreation::downloadError(QNetworkReply::NetworkError error) { qDebug() << m_reply->errorString(); - m_status = Job_Failed; + m_state = State::Failed; } void ImgurAlbumCreation::downloadFinished() { - if (m_status != Job_Failed) + if (m_state != State::Failed) { QByteArray data = m_reply->readAll(); m_reply.reset(); @@ -68,7 +68,7 @@ void ImgurAlbumCreation::downloadFinished() } m_deleteHash = object.value("data").toObject().value("deletehash").toString(); m_id = object.value("data").toObject().value("id").toString(); - m_status = Job_Finished; + m_state = State::Succeeded; emit succeeded(m_index_within_job); return; } @@ -82,7 +82,6 @@ void ImgurAlbumCreation::downloadFinished() } void ImgurAlbumCreation::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - m_total_progress = bytesTotal; - m_progress = bytesReceived; + setProgress(bytesReceived, bytesTotal); emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); } diff --git a/launcher/screenshots/ImgurAlbumCreation.h b/launcher/screenshots/ImgurAlbumCreation.h index cb048a23..4cb0ed5d 100644 --- a/launcher/screenshots/ImgurAlbumCreation.h +++ b/launcher/screenshots/ImgurAlbumCreation.h @@ -24,16 +24,14 @@ public: protected slots: - virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); - virtual void downloadError(QNetworkReply::NetworkError error); - virtual void downloadFinished(); - virtual void downloadReadyRead() - { - } + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; + void downloadError(QNetworkReply::NetworkError error) override; + void downloadFinished() override; + void downloadReadyRead() override {} public slots: - virtual void startImpl(); + void executeTask() override; private: QList m_screenshots; diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index 76a84947..0f0fd79c 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -13,13 +13,13 @@ ImgurUpload::ImgurUpload(ScreenShot::Ptr shot) : NetAction(), m_shot(shot) { m_url = BuildConfig.IMGUR_BASE_URL + "upload.json"; - m_status = Job_NotStarted; + m_state = State::Inactive; } -void ImgurUpload::startImpl() +void ImgurUpload::executeTask() { finished = false; - m_status = Job_InProgress; + m_state = Task::State::Running; QNetworkRequest request(m_url); request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); @@ -63,7 +63,7 @@ void ImgurUpload::downloadError(QNetworkReply::NetworkError error) qCritical() << "Double finished ImgurUpload!"; return; } - m_status = Job_Failed; + m_state = Task::State::Failed; finished = true; m_reply.reset(); emit failed(m_index_within_job); @@ -99,14 +99,13 @@ void ImgurUpload::downloadFinished() m_shot->m_imgurId = object.value("data").toObject().value("id").toString(); m_shot->m_url = object.value("data").toObject().value("link").toString(); m_shot->m_imgurDeleteHash = object.value("data").toObject().value("deletehash").toString(); - m_status = Job_Finished; + m_state = Task::State::Succeeded; finished = true; emit succeeded(m_index_within_job); return; } void ImgurUpload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - m_total_progress = bytesTotal; - m_progress = bytesReceived; + setProgress(bytesReceived, bytesTotal); emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); } diff --git a/launcher/screenshots/ImgurUpload.h b/launcher/screenshots/ImgurUpload.h index cf54f58d..a1040551 100644 --- a/launcher/screenshots/ImgurUpload.h +++ b/launcher/screenshots/ImgurUpload.h @@ -21,7 +21,7 @@ slots: public slots: - void startImpl() override; + void executeTask() override; private: ScreenShot::Ptr m_shot; diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 344a024e..61855160 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -52,6 +52,8 @@ class Task : public QObject { virtual bool canAbort() const { return false; } + auto getState() const -> State { return m_state; } + QString getStatus() { return m_status; } virtual auto getStepStatus() const -> QString { return m_status; } @@ -90,7 +92,7 @@ class Task : public QObject { void setStatus(const QString& status); void setProgress(qint64 current, qint64 total); - private: + protected: State m_state = State::Inactive; QStringList m_Warnings; QString m_failReason = ""; diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 250854d3..fbd17060 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -667,7 +667,7 @@ void TranslationsModel::downloadTranslation(QString key) auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + lang->file_name), entry); auto rawHash = QByteArray::fromHex(lang->file_sha1.toLatin1()); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); - dl->m_total_progress = lang->file_size; + dl->setProgress(dl->getProgress(), lang->file_size); d->m_dl_job = new NetJob("Translation for " + key, APPLICATION->network()); d->m_dl_job->addNetAction(dl); From efa3fbff39bf0dabebdf1c6330090ee320895a4d Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 26 Apr 2022 21:25:42 -0300 Subject: [PATCH 361/605] refactor: remove some superfluous signals Since now we're inheriting from Task, some signals can be reused. --- launcher/net/Download.cpp | 21 ++++++++++----------- launcher/net/NetAction.h | 10 ++-------- launcher/net/NetJob.cpp | 14 +++++++------- launcher/screenshots/ImgurAlbumCreation.cpp | 10 +++++----- launcher/screenshots/ImgurUpload.cpp | 12 ++++++------ launcher/tasks/Task.cpp | 3 +-- launcher/tasks/Task.h | 3 ++- 7 files changed, 33 insertions(+), 40 deletions(-) diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 5b5a04db..5e5d64fa 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -72,7 +72,7 @@ void Download::executeTask() { if (getState() == Task::State::AbortedByUser) { qWarning() << "Attempt to start an aborted Download:" << m_url.toString(); - emit aborted(m_index_within_job); + emitAborted(); return; } @@ -80,7 +80,7 @@ void Download::executeTask() m_state = m_sink->init(request); switch (m_state) { case State::Succeeded: - emit succeeded(m_index_within_job); + emit succeeded(); qDebug() << "Download cache hit " << m_url.toString(); return; case State::Running: @@ -88,7 +88,7 @@ void Download::executeTask() break; case State::Inactive: case State::Failed: - emit failed(m_index_within_job); + emitFailed(); return; case State::AbortedByUser: return; @@ -102,8 +102,8 @@ void Download::executeTask() QNetworkReply* rep = m_network->get(request); m_reply.reset(rep); - connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64))); - connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); + connect(rep, &QNetworkReply::downloadProgress, this, &Download::downloadProgress); + connect(rep, &QNetworkReply::finished, this, &Download::downloadFinished); connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); connect(rep, &QNetworkReply::sslErrors, this, &Download::sslErrors); connect(rep, &QNetworkReply::readyRead, this, &Download::downloadReadyRead); @@ -112,7 +112,6 @@ void Download::executeTask() void Download::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { setProgress(bytesReceived, bytesTotal); - emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); } void Download::downloadError(QNetworkReply::NetworkError error) @@ -212,19 +211,19 @@ void Download::downloadFinished() qDebug() << "Download failed but we are allowed to proceed:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emit succeeded(m_index_within_job); + emit succeeded(); return; } else if (m_state == State::Failed) { qDebug() << "Download failed in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); return; } else if (m_state == State::AbortedByUser) { qDebug() << "Download aborted in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emit aborted(m_index_within_job); + emitAborted(); return; } @@ -241,12 +240,12 @@ void Download::downloadFinished() qDebug() << "Download failed to finalize:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); return; } m_reply.reset(); qDebug() << "Download succeeded:" << m_url.toString(); - emit succeeded(m_index_within_job); + emit succeeded(); } void Download::downloadReadyRead() diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h index e15716f6..86a37ee6 100644 --- a/launcher/net/NetAction.h +++ b/launcher/net/NetAction.h @@ -43,7 +43,7 @@ class NetAction : public Task { Q_OBJECT protected: - explicit NetAction() : Task(nullptr) {}; + explicit NetAction() : Task() {}; public: using Ptr = shared_qobject_ptr; @@ -51,13 +51,7 @@ class NetAction : public Task { virtual ~NetAction() = default; QUrl url() { return m_url; } - - signals: - void started(int index); - void netActionProgress(int index, qint64 current, qint64 total); - void succeeded(int index); - void failed(int index); - void aborted(int index); + auto index() -> int { return m_index_within_job; } protected slots: virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0; diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index d08d6c4d..a9f89da4 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -45,9 +45,9 @@ auto NetJob::addNetAction(NetAction::Ptr action) -> bool partProgress(m_parts_progress.count() - 1, action->getProgress(), action->getTotalProgress()); if (action->isRunning()) { - connect(action.get(), &NetAction::succeeded, this, &NetJob::partSucceeded); - connect(action.get(), &NetAction::failed, this, &NetJob::partFailed); - connect(action.get(), &NetAction::netActionProgress, this, &NetJob::partProgress); + connect(action.get(), &NetAction::succeeded, [this, action]{ partSucceeded(action->index()); }); + connect(action.get(), &NetAction::failed, [this, action](QString){ partFailed(action->index()); }); + connect(action.get(), &NetAction::progress, [this, action](qint64 done, qint64 total) { partProgress(action->index(), done, total); }); } else { m_todo.append(m_parts_progress.size() - 1); } @@ -218,10 +218,10 @@ void NetJob::startMoreParts() auto part = m_downloads[doThis]; // connect signals :D - connect(part.get(), &NetAction::succeeded, this, &NetJob::partSucceeded); - connect(part.get(), &NetAction::failed, this, &NetJob::partFailed); - connect(part.get(), &NetAction::aborted, this, &NetJob::partAborted); - connect(part.get(), &NetAction::netActionProgress, this, &NetJob::partProgress); + connect(part.get(), &NetAction::succeeded, this, [this, part]{ partSucceeded(part->index()); }); + connect(part.get(), &NetAction::failed, this, [this, part](QString){ partFailed(part->index()); }); + connect(part.get(), &NetAction::aborted, this, [this, part]{ partAborted(part->index()); }); + connect(part.get(), &NetAction::progress, this, [this, part](qint64 done, qint64 total) { partProgress(part->index(), done, total); }); part->startAction(m_network); } diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index 81fac929..f94527c8 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -56,32 +56,32 @@ void ImgurAlbumCreation::downloadFinished() if (jsonError.error != QJsonParseError::NoError) { qDebug() << jsonError.errorString(); - emit failed(m_index_within_job); + emitFailed(); return; } auto object = doc.object(); if (!object.value("success").toBool()) { qDebug() << doc.toJson(); - emit failed(m_index_within_job); + emitFailed(); return; } m_deleteHash = object.value("data").toObject().value("deletehash").toString(); m_id = object.value("data").toObject().value("id").toString(); m_state = State::Succeeded; - emit succeeded(m_index_within_job); + emit succeeded(); return; } else { qDebug() << m_reply->readAll(); m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); return; } } void ImgurAlbumCreation::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { setProgress(bytesReceived, bytesTotal); - emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); + emit progress(bytesReceived, bytesTotal); } diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index 0f0fd79c..05314de7 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -28,7 +28,7 @@ void ImgurUpload::executeTask() QFile f(m_shot->m_file.absoluteFilePath()); if (!f.open(QFile::ReadOnly)) { - emit failed(m_index_within_job); + emitFailed(); return; } @@ -66,7 +66,7 @@ void ImgurUpload::downloadError(QNetworkReply::NetworkError error) m_state = Task::State::Failed; finished = true; m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); } void ImgurUpload::downloadFinished() { @@ -84,7 +84,7 @@ void ImgurUpload::downloadFinished() qDebug() << "imgur server did not reply with JSON" << jsonError.errorString(); finished = true; m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); return; } auto object = doc.object(); @@ -93,7 +93,7 @@ void ImgurUpload::downloadFinished() qDebug() << "Screenshot upload not successful:" << doc.toJson(); finished = true; m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); return; } m_shot->m_imgurId = object.value("data").toObject().value("id").toString(); @@ -101,11 +101,11 @@ void ImgurUpload::downloadFinished() m_shot->m_imgurDeleteHash = object.value("data").toObject().value("deletehash").toString(); m_state = Task::State::Succeeded; finished = true; - emit succeeded(m_index_within_job); + emit succeeded(); return; } void ImgurUpload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { setProgress(bytesReceived, bytesTotal); - emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); + emit progress(bytesReceived, bytesTotal); } diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index 57307b43..68e0e8a7 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -99,8 +99,7 @@ void Task::emitAborted() m_state = State::AbortedByUser; m_failReason = "Aborted."; qDebug() << "Task" << describe() << "aborted."; - emit failed(m_failReason); - emit finished(); + emit aborted(); } void Task::emitSucceeded() diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 61855160..e09c57ae 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -73,6 +73,7 @@ class Task : public QObject { virtual void progress(qint64 current, qint64 total); void finished(); void succeeded(); + void aborted(); void failed(QString reason); void status(QString status); @@ -86,7 +87,7 @@ class Task : public QObject { protected slots: virtual void emitSucceeded(); virtual void emitAborted(); - virtual void emitFailed(QString reason); + virtual void emitFailed(QString reason = ""); public slots: void setStatus(const QString& status); From 040ee919e5ea71364daa08c30e09c843976f5734 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 27 Apr 2022 18:36:11 -0300 Subject: [PATCH 362/605] refactor: more net cleanup This runs clang-tidy on some other files in launcher/net/. This also makes use of some JSON wrappers in HttpMetaCache, instead of using the Qt stuff directly. Lastly, this removes useless null checks (crashes don't occur because of this, but because of concurrent usage / free of the QByteArray pointer), and fix a fixme in Download.h --- launcher/net/ByteArraySink.h | 11 +- launcher/net/ChecksumValidator.h | 48 ++++---- launcher/net/Download.cpp | 24 ++-- launcher/net/Download.h | 59 ++++------ launcher/net/FileSink.cpp | 47 ++++---- launcher/net/HttpMetaCache.cpp | 190 ++++++++++++++----------------- launcher/net/HttpMetaCache.h | 107 +++++++---------- launcher/net/Mode.h | 9 +- launcher/net/Sink.h | 33 +++--- 9 files changed, 228 insertions(+), 300 deletions(-) diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index 75a66574..8ae30bb3 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -6,6 +6,8 @@ namespace Net { /* * Sink object for downloads that uses an external QByteArray it doesn't own as a target. + * FIXME: It is possible that the QByteArray is freed while we're doing some operation on it, + * causing a segmentation fault. */ class ByteArraySink : public Sink { public: @@ -16,9 +18,6 @@ class ByteArraySink : public Sink { public: auto init(QNetworkRequest& request) -> Task::State override { - if(!m_output) - return Task::State::Failed; - m_output->clear(); if (initAllValidators(request)) return Task::State::Running; @@ -27,9 +26,6 @@ class ByteArraySink : public Sink { auto write(QByteArray& data) -> Task::State override { - if(!m_output) - return Task::State::Failed; - m_output->append(data); if (writeAllValidators(data)) return Task::State::Running; @@ -38,9 +34,6 @@ class ByteArraySink : public Sink { auto abort() -> Task::State override { - if(!m_output) - return Task::State::Failed; - m_output->clear(); failAllValidators(); return Task::State::Failed; diff --git a/launcher/net/ChecksumValidator.h b/launcher/net/ChecksumValidator.h index 0d6b19c2..8a8b10d5 100644 --- a/launcher/net/ChecksumValidator.h +++ b/launcher/net/ChecksumValidator.h @@ -1,55 +1,47 @@ #pragma once #include "Validator.h" + #include -#include #include namespace Net { -class ChecksumValidator: public Validator -{ -public: /* con/des */ +class ChecksumValidator : public Validator { + public: ChecksumValidator(QCryptographicHash::Algorithm algorithm, QByteArray expected = QByteArray()) - :m_checksum(algorithm), m_expected(expected) - { - }; - virtual ~ChecksumValidator() {}; + : m_checksum(algorithm), m_expected(expected){}; + virtual ~ChecksumValidator() = default; -public: /* methods */ - bool init(QNetworkRequest &) override + public: + auto init(QNetworkRequest&) -> bool override { m_checksum.reset(); return true; } - bool write(QByteArray & data) override + + auto write(QByteArray& data) -> bool override { m_checksum.addData(data); return true; } - bool abort() override + + auto abort() -> bool override { return true; } + + auto validate(QNetworkReply&) -> bool override { - return true; - } - bool validate(QNetworkReply &) override - { - if(m_expected.size() && m_expected != hash()) - { + if (m_expected.size() && m_expected != hash()) { qWarning() << "Checksum mismatch, download is bad."; return false; } return true; } - QByteArray hash() - { - return m_checksum.result(); - } - void setExpected(QByteArray expected) - { - m_expected = expected; - } -private: /* data */ + auto hash() -> QByteArray { return m_checksum.result(); } + + void setExpected(QByteArray expected) { m_expected = expected; } + + private: QCryptographicHash m_checksum; QByteArray m_expected; }; -} \ No newline at end of file +} // namespace Net diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 5e5d64fa..3d6ca338 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -33,30 +33,29 @@ Download::Download() : NetAction() m_state = State::Inactive; } -Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) +auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Download::Ptr { - Download* dl = new Download(); + auto* dl = new Download(); dl->m_url = url; dl->m_options = options; auto md5Node = new ChecksumValidator(QCryptographicHash::Md5); auto cachedNode = new MetaCacheSink(entry, md5Node); dl->m_sink.reset(cachedNode); - dl->m_target_path = entry->getFullPath(); return dl; } -Download::Ptr Download::makeByteArray(QUrl url, QByteArray* output, Options options) +auto Download::makeByteArray(QUrl url, QByteArray* output, Options options) -> Download::Ptr { - Download* dl = new Download(); + auto* dl = new Download(); dl->m_url = url; dl->m_options = options; dl->m_sink.reset(new ByteArraySink(output)); return dl; } -Download::Ptr Download::makeFile(QUrl url, QString path, Options options) +auto Download::makeFile(QUrl url, QString path, Options options) -> Download::Ptr { - Download* dl = new Download(); + auto* dl = new Download(); dl->m_url = url; dl->m_options = options; dl->m_sink.reset(new FileSink(path)); @@ -143,7 +142,7 @@ void Download::sslErrors(const QList& errors) } } -bool Download::handleRedirect() +auto Download::handleRedirect() -> bool { QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); if (!redirect.isValid()) { @@ -230,7 +229,7 @@ void Download::downloadFinished() // make sure we got all the remaining data, if any auto data = m_reply->readAll(); if (data.size()) { - qDebug() << "Writing extra" << data.size() << "bytes to" << m_target_path; + qDebug() << "Writing extra" << data.size() << "bytes"; m_state = m_sink->write(data); } @@ -243,6 +242,7 @@ void Download::downloadFinished() emitFailed(); return; } + m_reply.reset(); qDebug() << "Download succeeded:" << m_url.toString(); emit succeeded(); @@ -254,17 +254,17 @@ void Download::downloadReadyRead() auto data = m_reply->readAll(); m_state = m_sink->write(data); if (m_state == State::Failed) { - qCritical() << "Failed to process response chunk for " << m_target_path; + qCritical() << "Failed to process response chunk"; } // qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes"; } else { - qCritical() << "Cannot write to " << m_target_path << ", illegal status" << m_status; + qCritical() << "Cannot write download data! illegal status " << m_status; } } } // namespace Net -bool Net::Download::abort() +auto Net::Download::abort() -> bool { if (m_reply) { m_reply->abort(); diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 231ad6a7..9fb67127 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -15,63 +15,54 @@ #pragma once -#include "NetAction.h" #include "HttpMetaCache.h" -#include "Validator.h" +#include "NetAction.h" #include "Sink.h" +#include "Validator.h" #include "QObjectPtr.h" namespace Net { -class Download : public NetAction -{ +class Download : public NetAction { Q_OBJECT -public: - typedef shared_qobject_ptr Ptr; - enum class Option - { - NoOptions = 0, - AcceptLocalFiles = 1 - }; + public: + using Ptr = shared_qobject_ptr; + enum class Option { NoOptions = 0, AcceptLocalFiles = 1 }; Q_DECLARE_FLAGS(Options, Option) -protected: + protected: explicit Download(); -public: - virtual ~Download(){}; - static Download::Ptr makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions); - static Download::Ptr makeByteArray(QUrl url, QByteArray *output, Options options = Option::NoOptions); - static Download::Ptr makeFile(QUrl url, QString path, Options options = Option::NoOptions); -public: - QString getTargetFilepath() - { - return m_target_path; - } - void addValidator(Validator * v); - bool abort() override; - bool canAbort() const override { return true; }; + public: + ~Download() override = default; -private: - bool handleRedirect(); + static auto makeCached(QUrl url, MetaEntryPtr entry, 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 slots: + public: + void addValidator(Validator* v); + auto abort() -> bool override; + auto canAbort() const -> bool override { return true; }; + + private: + auto handleRedirect() -> bool; + + protected slots: void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; void downloadError(QNetworkReply::NetworkError error) override; - void sslErrors(const QList & errors); + void sslErrors(const QList& errors); void downloadFinished() override; void downloadReadyRead() override; -public slots: + public slots: void executeTask() override; -private: - // FIXME: remove this, it has no business being here. - QString m_target_path; + private: std::unique_ptr m_sink; Options m_options; }; -} +} // namespace Net Q_DECLARE_OPERATORS_FOR_FLAGS(Net::Download::Options) diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp index 0d8b09bb..d2d2b06f 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -1,7 +1,5 @@ #include "FileSink.h" -#include - #include "FileSystem.h" namespace Net { @@ -9,44 +7,38 @@ namespace Net { Task::State FileSink::init(QNetworkRequest& request) { auto result = initCache(request); - if(result != Task::State::Running) - { + if (result != Task::State::Running) { return result; } + // create a new save file and open it for writing - if (!FS::ensureFilePathExists(m_filename)) - { + if (!FS::ensureFilePathExists(m_filename)) { qCritical() << "Could not create folder for " + m_filename; return Task::State::Failed; } + wroteAnyData = false; m_output_file.reset(new QSaveFile(m_filename)); - if (!m_output_file->open(QIODevice::WriteOnly)) - { + if (!m_output_file->open(QIODevice::WriteOnly)) { qCritical() << "Could not open " + m_filename + " for writing"; return Task::State::Failed; } - if(initAllValidators(request)) + if (initAllValidators(request)) return Task::State::Running; return Task::State::Failed; } -Task::State FileSink::initCache(QNetworkRequest &) -{ - return Task::State::Running; -} - Task::State FileSink::write(QByteArray& data) { - if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) - { + if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) { qCritical() << "Failed writing into " + m_filename; m_output_file->cancelWriting(); m_output_file.reset(); wroteAnyData = false; return Task::State::Failed; } + wroteAnyData = true; return Task::State::Running; } @@ -64,34 +56,39 @@ Task::State FileSink::finalize(QNetworkReply& reply) QVariant statusCodeV = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute); bool validStatus = false; int statusCode = statusCodeV.toInt(&validStatus); - if(validStatus) - { + if (validStatus) { // this leaves out 304 Not Modified gotFile = statusCode == 200 || statusCode == 203; } + // if we wrote any data to the save file, we try to commit the data to the real file. // if it actually got a proper file, we write it even if it was empty - if (gotFile || wroteAnyData) - { + if (gotFile || 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)) return Task::State::Failed; + // nothing went wrong... - if (!m_output_file->commit()) - { + if (!m_output_file->commit()) { qCritical() << "Failed to commit changes to " << m_filename; m_output_file->cancelWriting(); return Task::State::Failed; } } + // then get rid of the save file m_output_file.reset(); return finalizeCache(reply); } -Task::State FileSink::finalizeCache(QNetworkReply &) +Task::State FileSink::initCache(QNetworkRequest&) +{ + return Task::State::Running; +} + +Task::State FileSink::finalizeCache(QNetworkReply&) { return Task::State::Succeeded; } @@ -101,4 +98,4 @@ bool FileSink::hasLocalData() QFileInfo info(m_filename); return info.exists() && info.size() != 0; } -} +} // namespace Net diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index 8734e0bf..b41a18b1 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -15,29 +15,26 @@ #include "HttpMetaCache.h" #include "FileSystem.h" +#include "Json.h" -#include -#include -#include #include +#include +#include +#include #include -#include -#include -#include - -QString MetaEntry::getFullPath() +auto MetaEntry::getFullPath() -> QString { // FIXME: make local? return FS::PathCombine(basePath, relativePath); } -HttpMetaCache::HttpMetaCache(QString path) : QObject() +HttpMetaCache::HttpMetaCache(QString path) : QObject(), m_index_file(path) { - m_index_file = path; saveBatchingTimer.setSingleShot(true); saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer); + connect(&saveBatchingTimer, SIGNAL(timeout()), SLOT(SaveNow())); } @@ -47,45 +44,42 @@ HttpMetaCache::~HttpMetaCache() SaveNow(); } -MetaEntryPtr HttpMetaCache::getEntry(QString base, QString resource_path) +auto HttpMetaCache::getEntry(QString base, QString resource_path) -> MetaEntryPtr { // no base. no base path. can't store - if (!m_entries.contains(base)) - { + if (!m_entries.contains(base)) { // TODO: log problem - return MetaEntryPtr(); + return {}; } - EntryMap &map = m_entries[base]; - if (map.entry_list.contains(resource_path)) - { + + EntryMap& map = m_entries[base]; + if (map.entry_list.contains(resource_path)) { return map.entry_list[resource_path]; } - return MetaEntryPtr(); + + return {}; } -MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) +auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr { auto entry = getEntry(base, resource_path); // it's not present? generate a default stale entry - if (!entry) - { + if (!entry) { return staleEntry(base, resource_path); } - auto &selected_base = m_entries[base]; + auto& selected_base = m_entries[base]; QString real_path = FS::PathCombine(selected_base.base_path, resource_path); QFileInfo finfo(real_path); // is the file really there? if not -> stale - if (!finfo.isFile() || !finfo.isReadable()) - { + if (!finfo.isFile() || !finfo.isReadable()) { // if the file doesn't exist, we disown the entry selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } - if (!expected_etag.isEmpty() && expected_etag != entry->etag) - { + if (!expected_etag.isEmpty() && expected_etag != entry->etag) { // if the etag doesn't match expected, we disown the entry selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); @@ -93,18 +87,15 @@ MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QS // if the file changed, check md5sum qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch(); - if (file_last_changed != entry->local_changed_timestamp) - { + if (file_last_changed != entry->local_changed_timestamp) { QFile input(real_path); input.open(QIODevice::ReadOnly); - QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5) - .toHex() - .constData(); - if (entry->md5sum != md5sum) - { + QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData(); + if (entry->md5sum != md5sum) { selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } + // md5sums matched... keep entry and save the new state to file entry->local_changed_timestamp = file_last_changed; SaveEventually(); @@ -115,42 +106,42 @@ MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QS return entry; } -bool HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) +auto HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) -> bool { - if (!m_entries.contains(stale_entry->baseId)) - { - qCritical() << "Cannot add entry with unknown base: " - << stale_entry->baseId.toLocal8Bit(); + if (!m_entries.contains(stale_entry->baseId)) { + qCritical() << "Cannot add entry with unknown base: " << stale_entry->baseId.toLocal8Bit(); return false; } - if (stale_entry->stale) - { + + if (stale_entry->stale) { qCritical() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit(); return false; } + m_entries[stale_entry->baseId].entry_list[stale_entry->relativePath] = stale_entry; SaveEventually(); + + return true; +} + +auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool +{ + if (!entry) + return false; + + entry->stale = true; + SaveEventually(); return true; } -bool HttpMetaCache::evictEntry(MetaEntryPtr entry) -{ - if(entry) - { - entry->stale = true; - SaveEventually(); - return true; - } - return false; -} - -MetaEntryPtr HttpMetaCache::staleEntry(QString base, QString resource_path) +auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr { auto foo = new MetaEntry(); foo->baseId = base; foo->basePath = getBasePath(base); foo->relativePath = resource_path; foo->stale = true; + return MetaEntryPtr(foo); } @@ -159,24 +150,25 @@ void HttpMetaCache::addBase(QString base, QString base_root) // TODO: report error if (m_entries.contains(base)) return; + // TODO: check if the base path is valid EntryMap foo; foo.base_path = base_root; m_entries[base] = foo; } -QString HttpMetaCache::getBasePath(QString base) +auto HttpMetaCache::getBasePath(QString base) -> QString { - if (m_entries.contains(base)) - { + if (m_entries.contains(base)) { return m_entries[base].base_path; } - return QString(); + + return {}; } void HttpMetaCache::Load() { - if(m_index_file.isNull()) + if (m_index_file.isNull()) return; QFile index(m_index_file); @@ -184,41 +176,35 @@ void HttpMetaCache::Load() return; QJsonDocument json = QJsonDocument::fromJson(index.readAll()); - if (!json.isObject()) - return; - auto root = json.object(); + + auto root = Json::requireObject(json, "HttpMetaCache root"); + // check file version first - auto version_val = root.value("version"); - if (!version_val.isString()) - return; - if (version_val.toString() != "1") + auto version_val = Json::ensureString(root, "version"); + if (version_val != "1") return; // read the entry array - auto entries_val = root.value("entries"); - if (!entries_val.isArray()) - return; - QJsonArray array = entries_val.toArray(); - for (auto element : array) - { - if (!element.isObject()) - return; - auto element_obj = element.toObject(); - QString base = element_obj.value("base").toString(); + auto array = Json::ensureArray(root, "entries"); + for (auto element : array) { + auto element_obj = Json::ensureObject(element); + auto base = Json::ensureString(element_obj, "base"); if (!m_entries.contains(base)) continue; - auto &entrymap = m_entries[base]; + + auto& entrymap = m_entries[base]; + auto foo = new MetaEntry(); foo->baseId = base; - QString path = foo->relativePath = element_obj.value("path").toString(); - foo->md5sum = element_obj.value("md5sum").toString(); - foo->etag = element_obj.value("etag").toString(); - foo->local_changed_timestamp = element_obj.value("last_changed_timestamp").toDouble(); - foo->remote_changed_timestamp = - element_obj.value("remote_changed_timestamp").toString(); + foo->relativePath = Json::ensureString(element_obj, "path"); + foo->md5sum = Json::ensureString(element_obj, "md5sum"); + foo->etag = Json::ensureString(element_obj, "etag"); + foo->local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp"); + foo->remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp"); // presumed innocent until closer examination foo->stale = false; - entrymap.entry_list[path] = MetaEntryPtr(foo); + + entrymap.entry_list[foo->relativePath] = MetaEntryPtr(foo); } } @@ -231,42 +217,36 @@ void HttpMetaCache::SaveEventually() void HttpMetaCache::SaveNow() { - if(m_index_file.isNull()) + if (m_index_file.isNull()) return; + QJsonObject toplevel; - toplevel.insert("version", QJsonValue(QString("1"))); + Json::writeString(toplevel, "version", "1"); + QJsonArray entriesArr; - for (auto group : m_entries) - { - for (auto entry : group.entry_list) - { + for (auto group : m_entries) { + for (auto entry : group.entry_list) { // do not save stale entries. they are dead. - if(entry->stale) - { + if (entry->stale) { continue; } + QJsonObject entryObj; - entryObj.insert("base", QJsonValue(entry->baseId)); - entryObj.insert("path", QJsonValue(entry->relativePath)); - entryObj.insert("md5sum", QJsonValue(entry->md5sum)); - entryObj.insert("etag", QJsonValue(entry->etag)); - entryObj.insert("last_changed_timestamp", - QJsonValue(double(entry->local_changed_timestamp))); + Json::writeString(entryObj, "base", entry->baseId); + Json::writeString(entryObj, "path", entry->relativePath); + Json::writeString(entryObj, "md5sum", entry->md5sum); + Json::writeString(entryObj, "etag", entry->etag); + entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->local_changed_timestamp))); if (!entry->remote_changed_timestamp.isEmpty()) - entryObj.insert("remote_changed_timestamp", - QJsonValue(entry->remote_changed_timestamp)); + entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp)); entriesArr.append(entryObj); } } toplevel.insert("entries", entriesArr); - QJsonDocument doc(toplevel); - try - { - FS::write(m_index_file, doc.toJson()); - } - catch (const Exception &e) - { + try { + Json::write(toplevel, m_index_file); + } catch (const Exception& e) { qWarning() << e.what(); } } diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index 1c10e8c7..d8d1608e 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -14,109 +14,88 @@ */ #pragma once -#include -#include #include +#include +#include #include class HttpMetaCache; -class MetaEntry -{ -friend class HttpMetaCache; -protected: - MetaEntry() {} -public: - bool isStale() - { - return stale; - } - void setStale(bool stale) - { - this->stale = stale; - } - QString getFullPath(); - QString getRemoteChangedTimestamp() - { - return remote_changed_timestamp; - } - void setRemoteChangedTimestamp(QString remote_changed_timestamp) - { - this->remote_changed_timestamp = remote_changed_timestamp; - } - void setLocalChangedTimestamp(qint64 timestamp) - { - local_changed_timestamp = timestamp; - } - QString getETag() - { - return etag; - } - void setETag(QString etag) - { - this->etag = etag; - } - QString getMD5Sum() - { - return md5sum; - } - void setMD5Sum(QString md5sum) - { - this->md5sum = md5sum; - } -protected: +class MetaEntry { + friend class HttpMetaCache; + + protected: + MetaEntry() = default; + + public: + auto isStale() -> bool { return stale; } + void setStale(bool stale) { this->stale = stale; } + + auto getFullPath() -> QString; + + auto getRemoteChangedTimestamp() -> QString { return remote_changed_timestamp; } + void setRemoteChangedTimestamp(QString remote_changed_timestamp) { this->remote_changed_timestamp = remote_changed_timestamp; } + void setLocalChangedTimestamp(qint64 timestamp) { local_changed_timestamp = timestamp; } + + auto getETag() -> QString { return etag; } + void setETag(QString etag) { this->etag = etag; } + + auto getMD5Sum() -> QString { return md5sum; } + void setMD5Sum(QString md5sum) { this->md5sum = md5sum; } + + protected: QString baseId; QString basePath; QString relativePath; QString md5sum; QString etag; qint64 local_changed_timestamp = 0; - QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time + QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time bool stale = true; }; -typedef std::shared_ptr MetaEntryPtr; +using MetaEntryPtr = std::shared_ptr; -class HttpMetaCache : public QObject -{ +class HttpMetaCache : public QObject { Q_OBJECT -public: + public: // supply path to the cache index file HttpMetaCache(QString path = QString()); - ~HttpMetaCache(); + ~HttpMetaCache() override; // get the entry solely from the cache // you probably don't want this, unless you have some specific caching needs. - MetaEntryPtr getEntry(QString base, QString resource_path); + auto getEntry(QString base, QString resource_path) -> MetaEntryPtr; // get the entry from cache and verify that it isn't stale (within reason) - MetaEntryPtr resolveEntry(QString base, QString resource_path, - QString expected_etag = QString()); + auto resolveEntry(QString base, QString resource_path, QString expected_etag = QString()) -> MetaEntryPtr; // add a previously resolved stale entry - bool updateEntry(MetaEntryPtr stale_entry); + auto updateEntry(MetaEntryPtr stale_entry) -> bool; // evict selected entry from cache - bool evictEntry(MetaEntryPtr entry); + auto evictEntry(MetaEntryPtr entry) -> bool; void addBase(QString base, QString base_root); // (re)start a timer that calls SaveNow later. void SaveEventually(); void Load(); - QString getBasePath(QString base); -public -slots: + + auto getBasePath(QString base) -> QString; + + public slots: void SaveNow(); -private: + private: // create a new stale entry, given the parameters - MetaEntryPtr staleEntry(QString base, QString resource_path); - struct EntryMap - { + auto staleEntry(QString base, QString resource_path) -> MetaEntryPtr; + + struct EntryMap { QString base_path; QMap entry_list; }; + QMap m_entries; QString m_index_file; QTimer saveBatchingTimer; diff --git a/launcher/net/Mode.h b/launcher/net/Mode.h index 9a95f5ad..3d75981f 100644 --- a/launcher/net/Mode.h +++ b/launcher/net/Mode.h @@ -1,10 +1,5 @@ #pragma once -namespace Net -{ -enum class Mode -{ - Offline, - Online -}; +namespace Net { +enum class Mode { Offline, Online }; } diff --git a/launcher/net/Sink.h b/launcher/net/Sink.h index 3b2a7f8d..c8800220 100644 --- a/launcher/net/Sink.h +++ b/launcher/net/Sink.h @@ -8,14 +8,15 @@ namespace Net { class Sink { public: Sink() = default; - virtual ~Sink(){}; + virtual ~Sink() = default; public: - virtual Task::State init(QNetworkRequest& request) = 0; - virtual Task::State write(QByteArray& data) = 0; - virtual Task::State abort() = 0; - virtual Task::State finalize(QNetworkReply& reply) = 0; - virtual bool hasLocalData() = 0; + virtual auto init(QNetworkRequest& request) -> Task::State = 0; + virtual auto write(QByteArray& data) -> Task::State = 0; + virtual auto abort() -> Task::State = 0; + virtual auto finalize(QNetworkReply& reply) -> Task::State = 0; + + virtual auto hasLocalData() -> bool = 0; void addValidator(Validator* validator) { @@ -24,7 +25,15 @@ class Sink { } } - protected: /* methods */ + protected: + bool initAllValidators(QNetworkRequest& request) + { + for (auto& validator : validators) { + if (!validator->init(request)) + return false; + } + return true; + } bool finalizeAllValidators(QNetworkReply& reply) { for (auto& validator : validators) { @@ -41,14 +50,6 @@ class Sink { } return success; } - bool initAllValidators(QNetworkRequest& request) - { - for (auto& validator : validators) { - if (!validator->init(request)) - return false; - } - return true; - } bool writeAllValidators(QByteArray& data) { for (auto& validator : validators) { @@ -58,7 +59,7 @@ class Sink { return true; } - protected: /* data */ + protected: std::vector> validators; }; } // namespace Net From 57d65177c8ebb5463c88dd8e26f1e0a33f648bed Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 1 May 2022 11:05:31 -0300 Subject: [PATCH 363/605] fix: abort and fail logic in tasks Also sets up correctly the status connections --- launcher/net/Download.cpp | 9 ++++++--- launcher/net/NetJob.cpp | 3 +++ launcher/tasks/Task.cpp | 1 + launcher/tasks/Task.h | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 3d6ca338..9c01fa8d 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -69,6 +69,8 @@ void Download::addValidator(Validator* v) void Download::executeTask() { + setStatus(tr("Downloading %1").arg(m_url.toString())); + if (getState() == Task::State::AbortedByUser) { qWarning() << "Attempt to start an aborted Download:" << m_url.toString(); emitAborted(); @@ -90,6 +92,7 @@ void Download::executeTask() emitFailed(); return; case State::AbortedByUser: + emitAborted(); return; } @@ -216,13 +219,13 @@ void Download::downloadFinished() qDebug() << "Download failed in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emitFailed(); + emit failed(""); return; } else if (m_state == State::AbortedByUser) { qDebug() << "Download aborted in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emitAborted(); + emit aborted(); return; } @@ -239,7 +242,7 @@ void Download::downloadFinished() qDebug() << "Download failed to finalize:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emitFailed(); + emit failed(""); return; } diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index a9f89da4..906a735f 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -47,7 +47,9 @@ auto NetJob::addNetAction(NetAction::Ptr action) -> bool if (action->isRunning()) { connect(action.get(), &NetAction::succeeded, [this, action]{ partSucceeded(action->index()); }); connect(action.get(), &NetAction::failed, [this, action](QString){ partFailed(action->index()); }); + connect(action.get(), &NetAction::aborted, [this, action](){ partAborted(action->index()); }); connect(action.get(), &NetAction::progress, [this, action](qint64 done, qint64 total) { partProgress(action->index(), done, total); }); + connect(action.get(), &NetAction::status, this, &NetJob::status); } else { m_todo.append(m_parts_progress.size() - 1); } @@ -222,6 +224,7 @@ void NetJob::startMoreParts() connect(part.get(), &NetAction::failed, this, [this, part](QString){ partFailed(part->index()); }); connect(part.get(), &NetAction::aborted, this, [this, part]{ partAborted(part->index()); }); connect(part.get(), &NetAction::progress, this, [this, part](qint64 done, qint64 total) { partProgress(part->index(), done, total); }); + connect(part.get(), &NetAction::status, this, &NetJob::status); part->startAction(m_network); } diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index 68e0e8a7..d2d62c9e 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -100,6 +100,7 @@ void Task::emitAborted() m_failReason = "Aborted."; qDebug() << "Task" << describe() << "aborted."; emit aborted(); + emit finished(); } void Task::emitSucceeded() diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index e09c57ae..0ca37e02 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -79,7 +79,7 @@ class Task : public QObject { public slots: virtual void start(); - virtual bool abort() { return false; }; + virtual bool abort() { if(canAbort()) emitAborted(); return canAbort(); }; protected: virtual void executeTask() = 0; From 0bce08d30f2bbdeca19c375840880f69ffeac81b Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 2 May 2022 12:56:24 -0300 Subject: [PATCH 364/605] chore: add polymc license headers to launcher/net files --- launcher/net/ByteArraySink.h | 35 ++++++++++++++++++++++++++ launcher/net/ChecksumValidator.h | 35 ++++++++++++++++++++++++++ launcher/net/Download.cpp | 41 ++++++++++++++++++++++-------- launcher/net/Download.h | 40 +++++++++++++++++++++-------- launcher/net/FileSink.cpp | 35 ++++++++++++++++++++++++++ launcher/net/FileSink.h | 35 ++++++++++++++++++++++++++ launcher/net/HttpMetaCache.cpp | 40 +++++++++++++++++++++-------- launcher/net/HttpMetaCache.h | 43 ++++++++++++++++++++++++-------- launcher/net/MetaCacheSink.cpp | 35 ++++++++++++++++++++++++++ launcher/net/MetaCacheSink.h | 35 ++++++++++++++++++++++++++ launcher/net/NetAction.h | 1 + launcher/net/NetJob.cpp | 1 + launcher/net/NetJob.h | 1 + launcher/net/PasteUpload.cpp | 35 ++++++++++++++++++++++++++ launcher/net/PasteUpload.h | 36 ++++++++++++++++++++++++++ launcher/net/Sink.h | 35 ++++++++++++++++++++++++++ launcher/net/Validator.h | 35 ++++++++++++++++++++++++++ 17 files changed, 476 insertions(+), 42 deletions(-) diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index 8ae30bb3..501318a1 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 "Sink.h" diff --git a/launcher/net/ChecksumValidator.h b/launcher/net/ChecksumValidator.h index 8a8b10d5..a2ca2c7a 100644 --- a/launcher/net/ChecksumValidator.h +++ b/launcher/net/ChecksumValidator.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 "Validator.h" diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 9c01fa8d..97033de1 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -1,22 +1,41 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "Download.h" #include -#include #include #include "ByteArraySink.h" diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 9fb67127..20932944 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp index d2d2b06f..ba0caf6c 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 "FileSink.h" #include "FileSystem.h" diff --git a/launcher/net/FileSink.h b/launcher/net/FileSink.h index 9d77b3d0..dffbdca6 100644 --- a/launcher/net/FileSink.h +++ b/launcher/net/FileSink.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index b41a18b1..4d86c0b8 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "HttpMetaCache.h" diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index d8d1608e..e944b3d5 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -1,22 +1,43 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 class HttpMetaCache; diff --git a/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp index 34ba9f56..f86dd870 100644 --- a/launcher/net/MetaCacheSink.cpp +++ b/launcher/net/MetaCacheSink.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 "MetaCacheSink.h" #include #include diff --git a/launcher/net/MetaCacheSink.h b/launcher/net/MetaCacheSink.h index 431e10a8..c9f7edfe 100644 --- a/launcher/net/MetaCacheSink.h +++ b/launcher/net/MetaCacheSink.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 "ChecksumValidator.h" diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h index 86a37ee6..729d4132 100644 --- a/launcher/net/NetAction.h +++ b/launcher/net/NetAction.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - 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 diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index 906a735f..df899178 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - 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 diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h index c397e2a1..63c1cf51 100644 --- a/launcher/net/NetJob.h +++ b/launcher/net/NetJob.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - 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 diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 52b82a0e..e88c8987 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 "PasteUpload.h" #include "BuildConfig.h" #include "Application.h" diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index 62b2dc36..53979352 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -1,4 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 "tasks/Task.h" #include #include diff --git a/launcher/net/Sink.h b/launcher/net/Sink.h index c8800220..3870f29b 100644 --- a/launcher/net/Sink.h +++ b/launcher/net/Sink.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 "net/NetAction.h" diff --git a/launcher/net/Validator.h b/launcher/net/Validator.h index 59b72a0b..e1d71d1c 100644 --- a/launcher/net/Validator.h +++ b/launcher/net/Validator.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 "net/NetAction.h" From dd2b324d8f7081f52decd90210ce11ef37625315 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 2 May 2022 14:33:21 -0300 Subject: [PATCH 365/605] chore: add license header to remaining files Also remove some unused imports --- launcher/InstanceImportTask.cpp | 40 ++++++++++++++----- launcher/minecraft/AssetsUtils.cpp | 40 ++++++++++++++----- launcher/screenshots/ImgurAlbumCreation.cpp | 35 ++++++++++++++++ launcher/screenshots/ImgurAlbumCreation.h | 37 ++++++++++++++++- launcher/screenshots/ImgurUpload.cpp | 35 ++++++++++++++++ launcher/screenshots/ImgurUpload.h | 37 ++++++++++++++++- launcher/tasks/Task.cpp | 40 ++++++++++++++----- launcher/tasks/Task.h | 44 ++++++++++++++------- launcher/translations/TranslationsModel.cpp | 35 ++++++++++++++++ 9 files changed, 297 insertions(+), 46 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index fc3432c1..ca7e0590 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "InstanceImportTask.h" diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 281f730f..15062c2b 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index f94527c8..7afdc5cc 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 "ImgurAlbumCreation.h" #include diff --git a/launcher/screenshots/ImgurAlbumCreation.h b/launcher/screenshots/ImgurAlbumCreation.h index 4cb0ed5d..0228b6e4 100644 --- a/launcher/screenshots/ImgurAlbumCreation.h +++ b/launcher/screenshots/ImgurAlbumCreation.h @@ -1,7 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 "net/NetAction.h" #include "Screenshot.h" -#include "QObjectPtr.h" typedef shared_qobject_ptr ImgurAlbumCreationPtr; class ImgurAlbumCreation : public NetAction diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index 05314de7..fbcfb95f 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 "ImgurUpload.h" #include "BuildConfig.h" diff --git a/launcher/screenshots/ImgurUpload.h b/launcher/screenshots/ImgurUpload.h index a1040551..404dc876 100644 --- a/launcher/screenshots/ImgurUpload.h +++ b/launcher/screenshots/ImgurUpload.h @@ -1,5 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 "QObjectPtr.h" + #include "net/NetAction.h" #include "Screenshot.h" diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index d2d62c9e..bb71b98c 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "Task.h" diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 0ca37e02..f0e6e402 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -1,24 +1,40 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "QObjectPtr.h" class Task : public QObject { diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index fbd17060..53722d69 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 + * + * 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 "TranslationsModel.h" #include From 067484a6a8647e6012f3fdad61653716cfb44470 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Fri, 13 May 2022 16:59:00 +0100 Subject: [PATCH 366/605] Fix formatting --- libraries/launcher/org/multimc/EntryPoint.java | 4 +++- libraries/launcher/org/multimc/applet/LegacyFrame.java | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index 416f2189..0244a04d 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -1,4 +1,4 @@ -package org.multimc;/* +/* * Copyright 2012-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +14,8 @@ package org.multimc;/* * limitations under the License. */ +package org.multimc; + import org.multimc.exception.ParseException; import org.multimc.utils.Parameters; diff --git a/libraries/launcher/org/multimc/applet/LegacyFrame.java b/libraries/launcher/org/multimc/applet/LegacyFrame.java index f82cb605..caec079c 100644 --- a/libraries/launcher/org/multimc/applet/LegacyFrame.java +++ b/libraries/launcher/org/multimc/applet/LegacyFrame.java @@ -1,4 +1,4 @@ -package org.multimc.applet;/* +/* * Copyright 2012-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +14,8 @@ package org.multimc.applet;/* * limitations under the License. */ +package org.multimc.applet; + import net.minecraft.Launcher; import javax.imageio.ImageIO; From c054d0f329a9d1d3ae76a605d82f0ad8e0ebdc99 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Fri, 13 May 2022 17:21:35 +0100 Subject: [PATCH 367/605] Add the license header to LauncherFactory --- .../launcher/org/multimc/LauncherFactory.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/libraries/launcher/org/multimc/LauncherFactory.java b/libraries/launcher/org/multimc/LauncherFactory.java index 17e0d905..007ce7e8 100644 --- a/libraries/launcher/org/multimc/LauncherFactory.java +++ b/libraries/launcher/org/multimc/LauncherFactory.java @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 icelimetea, + * + * 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. + */ + package org.multimc; import org.multimc.impl.OneSixLauncher; From c3336251e0789fae6da5935c0e2b7f38eab08763 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Fri, 13 May 2022 18:10:11 +0100 Subject: [PATCH 368/605] Add the license header to EntryPoint --- .../launcher/org/multimc/EntryPoint.java | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index 0244a04d..ba5b0926 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -1,17 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2012-2021 MultiMC Contributors + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 icelimetea, * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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. */ package org.multimc; From 84b962f256a492ae9a82846be40b726c8bd90e9c Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 13 May 2022 17:21:35 -0300 Subject: [PATCH 369/605] fix: Handle icons with a dot in their names E.g. some FTB modpacks. Also fixes an issue with the name viewing on the Icon Chooser dialog when the name was too big. --- launcher/icons/IconList.cpp | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index 584edd69..c269d10a 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -36,7 +36,7 @@ IconList::IconList(const QStringList &builtinPaths, QString path, QObject *paren auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name); for (auto file_info : file_info_list) { - builtinNames.insert(file_info.baseName()); + builtinNames.insert(file_info.completeBaseName()); } } for(auto & builtinName : builtinNames) @@ -51,6 +51,9 @@ IconList::IconList(const QStringList &builtinPaths, QString path, QObject *paren connect(m_watcher.get(), SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString))); directoryChanged(path); + + // Forces the UI to update, so that lengthy icon names are shown properly from the start + emit iconUpdated({}); } void IconList::directoryChanged(const QString &path) @@ -94,7 +97,13 @@ void IconList::directoryChanged(const QString &path) { qDebug() << "Removing " << remove; QFileInfo rmfile(remove); - QString key = rmfile.baseName(); + 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 (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif") + key = rmfile.fileName(); + int idx = getIconIndex(key); if (idx == -1) continue; @@ -117,8 +126,15 @@ void IconList::directoryChanged(const QString &path) for (auto add : to_add) { qDebug() << "Adding " << add; + QFileInfo addfile(add); - QString key = addfile.baseName(); + QString key = addfile.completeBaseName(); + + 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 (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif") + key = addfile.fileName(); + if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased)) { m_watcher->addPath(add); @@ -133,7 +149,7 @@ void IconList::fileChanged(const QString &path) QFileInfo checkfile(path); if (!checkfile.exists()) return; - QString key = checkfile.baseName(); + QString key = checkfile.completeBaseName(); int idx = getIconIndex(key); if (idx == -1) return; From fac0b027b31ba2ac29730f6091b3d19ba78b40d2 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Sat, 14 May 2022 16:46:57 +0100 Subject: [PATCH 370/605] Fix the license header --- .../launcher/org/multimc/LauncherFactory.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/libraries/launcher/org/multimc/LauncherFactory.java b/libraries/launcher/org/multimc/LauncherFactory.java index 007ce7e8..1b30a415 100644 --- a/libraries/launcher/org/multimc/LauncherFactory.java +++ b/libraries/launcher/org/multimc/LauncherFactory.java @@ -14,23 +14,6 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package org.multimc; From 3f259eb97a207c6d4d0ae3ad481541eda96df798 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Sat, 14 May 2022 16:48:14 +0100 Subject: [PATCH 371/605] Refactor script parsing --- .../launcher/org/multimc/EntryPoint.java | 43 ++++++------------- .../launcher/org/multimc/LauncherFactory.java | 4 +- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index ba5b0926..c0500bbe 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -51,8 +51,6 @@ public final class EntryPoint { private final Parameters params = new Parameters(); - private String launcherType; - public static void main(String[] args) { EntryPoint listener = new EntryPoint(); @@ -80,15 +78,6 @@ public final class EntryPoint { return Action.Abort; } - case "launcher": { - if (tokens.length != 2) - throw new ParseException("Expected 2 tokens, got " + tokens.length); - - launcherType = tokens[1]; - - return Action.Proceed; - } - default: { if (tokens.length != 2) throw new ParseException("Error while parsing:" + inData); @@ -129,30 +118,24 @@ public final class EntryPoint { return 1; } - if (launcherType != null) { - try { - Launcher launcher = - LauncherFactory - .getInstance() - .createLauncher(launcherType, params); + try { + Launcher launcher = + LauncherFactory + .getInstance() + .createLauncher(params); - launcher.launch(); + launcher.launch(); - return 0; - } catch (IllegalArgumentException e) { - LOGGER.log(Level.SEVERE, "Wrong argument.", e); + return 0; + } catch (IllegalArgumentException e) { + LOGGER.log(Level.SEVERE, "Wrong argument.", e); - return 1; - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Exception caught from launcher.", e); + return 1; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Exception caught from launcher.", e); - return 1; - } + return 1; } - - LOGGER.log(Level.SEVERE, "No valid launcher implementation specified."); - - return 1; } private enum Action { diff --git a/libraries/launcher/org/multimc/LauncherFactory.java b/libraries/launcher/org/multimc/LauncherFactory.java index 1b30a415..a2af8581 100644 --- a/libraries/launcher/org/multimc/LauncherFactory.java +++ b/libraries/launcher/org/multimc/LauncherFactory.java @@ -39,7 +39,9 @@ public final class LauncherFactory { }); } - public Launcher createLauncher(String name, Parameters parameters) { + public Launcher createLauncher(Parameters parameters) { + String name = parameters.first("launcher"); + LauncherProvider launcherProvider = launcherRegistry.get(name); if (launcherProvider == null) From c6b3eccbdf2785b59ab33ed99fabf6f3f5d81d2a Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 14 May 2022 19:46:52 +0200 Subject: [PATCH 372/605] refactor: rename Modrinth classes to ModrinthMod --- launcher/CMakeLists.txt | 8 ++++---- launcher/ui/dialogs/ModDownloadDialog.cpp | 4 ++-- launcher/ui/dialogs/ModDownloadDialog.h | 4 ++-- .../{ModrinthModel.cpp => ModrinthModModel.cpp} | 2 +- .../{ModrinthModel.h => ModrinthModModel.h} | 4 ++-- .../{ModrinthPage.cpp => ModrinthModPage.cpp} | 16 ++++++++-------- .../{ModrinthPage.h => ModrinthModPage.h} | 6 +++--- 7 files changed, 22 insertions(+), 22 deletions(-) rename launcher/ui/pages/modplatform/modrinth/{ModrinthModel.cpp => ModrinthModModel.cpp} (97%) rename launcher/ui/pages/modplatform/modrinth/{ModrinthModel.h => ModrinthModModel.h} (85%) rename launcher/ui/pages/modplatform/modrinth/{ModrinthPage.cpp => ModrinthModPage.cpp} (85%) rename launcher/ui/pages/modplatform/modrinth/{ModrinthPage.h => ModrinthModPage.h} (93%) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index b79f03c8..16ec4c04 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -782,10 +782,10 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/ImportPage.cpp ui/pages/modplatform/ImportPage.h - ui/pages/modplatform/modrinth/ModrinthModel.cpp - ui/pages/modplatform/modrinth/ModrinthModel.h - ui/pages/modplatform/modrinth/ModrinthPage.cpp - ui/pages/modplatform/modrinth/ModrinthPage.h + ui/pages/modplatform/modrinth/ModrinthModModel.cpp + ui/pages/modplatform/modrinth/ModrinthModModel.h + ui/pages/modplatform/modrinth/ModrinthModPage.cpp + ui/pages/modplatform/modrinth/ModrinthModPage.h # GUI - dialogs ui/dialogs/AboutDialog.cpp diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index d02ea476..305e85c0 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -13,7 +13,7 @@ #include #include "ui/widgets/PageContainer.h" -#include "ui/pages/modplatform/modrinth/ModrinthPage.h" +#include "ui/pages/modplatform/modrinth/ModrinthModPage.h" #include "ModDownloadTask.h" @@ -98,7 +98,7 @@ void ModDownloadDialog::accept() QList ModDownloadDialog::getPages() { - modrinthPage = new ModrinthPage(this, m_instance); + modrinthPage = new ModrinthModPage(this, m_instance); flameModPage = new FlameModPage(this, m_instance); return { diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 309d89d0..782dc361 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -16,7 +16,7 @@ class ModDownloadDialog; class PageContainer; class QDialogButtonBox; -class ModrinthPage; +class ModrinthModPage; class ModDownloadDialog : public QDialog, public BasePageProvider { @@ -50,7 +50,7 @@ private: QVBoxLayout *m_verticalLayout = nullptr; - ModrinthPage *modrinthPage = nullptr; + ModrinthModPage *modrinthPage = nullptr; FlameModPage *flameModPage = nullptr; QHash modTask; BaseInstance *m_instance; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp similarity index 97% rename from launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp rename to launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp index b788860a..1d9f4d60 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -#include "ModrinthModel.h" +#include "ModrinthModModel.h" #include "modplatform/modrinth/ModrinthPackIndex.h" diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h similarity index 85% rename from launcher/ui/pages/modplatform/modrinth/ModrinthModel.h rename to launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h index 45a6090a..63c23bbe 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h @@ -1,6 +1,6 @@ #pragma once -#include "ModrinthPage.h" +#include "ModrinthModPage.h" namespace Modrinth { @@ -8,7 +8,7 @@ class ListModel : public ModPlatform::ListModel { Q_OBJECT public: - ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent){}; + ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent){}; ~ListModel() override = default; private: diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp similarity index 85% rename from launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp rename to launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp index 98bde0ae..d3a1f859 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp @@ -33,14 +33,14 @@ * limitations under the License. */ -#include "ModrinthPage.h" +#include "ModrinthModPage.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "ui_ModPage.h" -#include "ModrinthModel.h" +#include "ModrinthModModel.h" #include "ui/dialogs/ModDownloadDialog.h" -ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) +ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance) : ModPage(dialog, instance, new ModrinthAPI()) { listModel = new Modrinth::ListModel(this); @@ -56,12 +56,12 @@ ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) // 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(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); - connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthPage::onModSelected); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged); + connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onModSelected); } -auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool +auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool { auto loaderStrings = ModrinthAPI::getModLoaderStrings(loader); @@ -79,4 +79,4 @@ auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString min // 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... -auto ModrinthPage::shouldDisplay() const -> bool { return true; } +auto ModrinthModPage::shouldDisplay() const -> bool { return true; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h similarity index 93% rename from launcher/ui/pages/modplatform/modrinth/ModrinthPage.h rename to launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h index e3a0e1f0..b1e72bfe 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h @@ -40,12 +40,12 @@ #include "modplatform/modrinth/ModrinthAPI.h" -class ModrinthPage : public ModPage { +class ModrinthModPage : public ModPage { Q_OBJECT public: - explicit ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance); - ~ModrinthPage() override = default; + explicit ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance); + ~ModrinthModPage() override = default; inline auto displayName() const -> QString override { return "Modrinth"; } inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); } From db038463581400005f045a277a249ab07175ab2b Mon Sep 17 00:00:00 2001 From: kb1000 Date: Mon, 31 Jan 2022 15:25:36 +0100 Subject: [PATCH 373/605] Add support for importing Modrinth packs from files --- launcher/CMakeLists.txt | 10 ++ launcher/InstanceImportTask.cpp | 170 +++++++++++++++++- launcher/InstanceImportTask.h | 6 +- .../modrinth/ModrinthPackManifest.cpp | 16 ++ .../modrinth/ModrinthPackManifest.h | 32 ++++ launcher/resources/multimc/multimc.qrc | 3 + .../resources/multimc/scalable/modrinth.svg | 4 + launcher/ui/dialogs/NewInstanceDialog.cpp | 2 + launcher/ui/pages/modplatform/ImportPage.cpp | 4 +- .../modplatform/modrinth/ModrinthPage.cpp | 55 ++++++ .../pages/modplatform/modrinth/ModrinthPage.h | 62 +++++++ .../modplatform/modrinth/ModrinthPage.ui | 94 ++++++++++ 12 files changed, 452 insertions(+), 6 deletions(-) create mode 100644 launcher/modplatform/modrinth/ModrinthPackManifest.cpp create mode 100644 launcher/modplatform/modrinth/ModrinthPackManifest.h create mode 100644 launcher/resources/multimc/scalable/modrinth.svg create mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp create mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthPage.h create mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 16ec4c04..cbe135e2 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -563,6 +563,11 @@ set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLShareCode.h ) +set(MODRINTH_SOURCES + modplatform/modrinth/ModrinthPackManifest.cpp + modplatform/modrinth/ModrinthPackManifest.h +) + add_unit_test(Index SOURCES meta/Index_test.cpp LIBS Launcher_logic @@ -596,6 +601,7 @@ set(LOGIC_SOURCES ${MODPACKSCH_SOURCES} ${TECHNIC_SOURCES} ${ATLAUNCHER_SOURCES} + ${MODRINTH_SOURCES} ) SET(LAUNCHER_SOURCES @@ -774,6 +780,9 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/flame/FlameModPage.cpp ui/pages/modplatform/flame/FlameModPage.h + ui/pages/modplatform/modrinth/ModrinthPage.cpp + ui/pages/modplatform/modrinth/ModrinthPage.h + ui/pages/modplatform/technic/TechnicModel.cpp ui/pages/modplatform/technic/TechnicModel.h ui/pages/modplatform/technic/TechnicPage.cpp @@ -908,6 +917,7 @@ qt5_wrap_ui(LAUNCHER_UI ui/pages/modplatform/legacy_ftb/Page.ui ui/pages/modplatform/ImportPage.ui ui/pages/modplatform/ftb/FtbPage.ui + ui/pages/modplatform/modrinth/ModrinthPage.ui ui/pages/modplatform/technic/TechnicPage.ui ui/widgets/InstanceCardWidget.ui ui/widgets/CustomCommands.ui diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 1a13c997..51715581 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -30,10 +30,15 @@ #include "modplatform/flame/PackManifest.h" #include "Json.h" #include +#include "modplatform/modrinth/ModrinthPackManifest.h" #include "modplatform/technic/TechnicPackProcessor.h" #include "icons/IconList.h" #include "Application.h" +#include "net/ChecksumValidator.h" + +#include +#include InstanceImportTask::InstanceImportTask(const QUrl sourceUrl) { @@ -109,6 +114,7 @@ void InstanceImportTask::processZipPack() QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") || QuaZipDir(m_packZip.get()).exists("/bin/version.json"); QString flameFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json"); + QString modrinthFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "modrinth.index.json"); QString root; if(!mmcFound.isNull()) { @@ -132,6 +138,13 @@ void InstanceImportTask::processZipPack() root = flameFound; m_modpackType = ModpackType::Flame; } + else if(!modrinthFound.isNull()) + { + // process as Modrinth pack + qDebug() << "Modrinth:" << modrinthFound; + root = modrinthFound; + m_modpackType = ModpackType::Modrinth; + } if(m_modpackType == ModpackType::Unknown) { emitFailed(tr("Archive does not contain a recognized modpack type.")); @@ -188,15 +201,18 @@ void InstanceImportTask::extractFinished() switch(m_modpackType) { - case ModpackType::Flame: - processFlame(); - return; case ModpackType::MultiMC: processMultiMC(); return; case ModpackType::Technic: processTechnic(); return; + case ModpackType::Flame: + processFlame(); + return; + case ModpackType::Modrinth: + processModrinth(); + return; case ModpackType::Unknown: emitFailed(tr("Archive does not contain a recognized modpack type.")); return; @@ -461,3 +477,151 @@ void InstanceImportTask::processMultiMC() } emitSucceeded(); } + +void InstanceImportTask::processModrinth() { + std::vector files; + QString minecraftVersion, fabricVersion, forgeVersion; + try + { + QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + auto doc = Json::requireDocument(indexPath); + auto obj = Json::requireObject(doc, "modrinth.index.json"); + int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json"); + if (formatVersion == 1) + { + auto game = Json::requireString(obj, "game", "modrinth.index.json"); + if (game != "minecraft") + { + throw JSONValidationError("Unknown game: " + game); + } + + auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); + std::transform(jsonFiles.begin(), jsonFiles.end(), std::back_inserter(files), [](const QJsonObject& obj) + { + Modrinth::File file; + file.path = Json::requireString(obj, "path"); + QString supported = Json::ensureString(Json::ensureObject(obj, "env")); + QJsonObject hashes = Json::requireObject(obj, "hashes"); + QString hash; + QCryptographicHash::Algorithm hashAlgorithm; + hash = Json::ensureString(hashes, "sha256"); + hashAlgorithm = QCryptographicHash::Sha256; + if (hash.isEmpty()) + { + hash = Json::ensureString(hashes, "sha512"); + hashAlgorithm = QCryptographicHash::Sha512; + if (hash.isEmpty()) + { + hash = Json::ensureString(hashes, "sha1"); + hashAlgorithm = QCryptographicHash::Sha1; + if (hash.isEmpty()) + { + throw JSONValidationError("No hash found for: " + file.path); + } + } + } + file.hash = QByteArray::fromHex(hash.toLatin1()); + file.hashAlgorithm = hashAlgorithm; + // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly handle spaces) + file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); + if (!file.download.isValid()) + { + throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); + } + return file; + }); + + auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); + for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) + { + QString name = it.key(); + if (name == "minecraft") + { + if (!minecraftVersion.isEmpty()) + throw JSONValidationError("Duplicate Minecraft version"); + minecraftVersion = Json::requireString(*it, "Minecraft version"); + } + else if (name == "fabric-loader") + { + if (!fabricVersion.isEmpty()) + throw JSONValidationError("Duplicate Fabric Loader version"); + fabricVersion = Json::requireString(*it, "Fabric Loader version"); + } + else if (name == "forge") + { + if (!forgeVersion.isEmpty()) + throw JSONValidationError("Duplicate Forge version"); + forgeVersion = Json::requireString(*it, "Forge version"); + } + else + { + throw JSONValidationError("Unknown dependency type: " + name); + } + } + } + else + { + throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion)); + } + QFile::remove(indexPath); + } + catch (const JSONValidationError &e) + { + emitFailed(tr("Could not understand pack index:\n") + e.cause()); + return; + } + QString overridePath = FS::PathCombine(m_stagingPath, "overrides"); + if (QFile::exists(overridePath)) { + QString mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); + if (!QFile::rename(overridePath, mcPath)) { + emitFailed(tr("Could not rename the overrides folder:\n") + "overrides"); + return; + } + } + + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(configPath); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", minecraftVersion, true); + if (!fabricVersion.isEmpty()) + components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion, true); + if (!forgeVersion.isEmpty()) + components->setComponentVersion("net.minecraftforge", forgeVersion, true); + if (m_instIcon != "default") + { + instance.setIconKey(m_instIcon); + } + instance.setName(m_instName); + instance.saveNow(); + + m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); + for (auto &file : files) + { + auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path); + qDebug() << "Will download" << file.download << "to" << path; + auto dl = Net::Download::makeFile(file.download, path); + dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); + m_filesNetJob->addNetAction(dl); + } + connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() + { + m_filesNetJob.reset(); + emitSucceeded(); + } + ); + connect(m_filesNetJob.get(), &NetJob::failed, [&](const QString &reason) + { + m_filesNetJob.reset(); + emitFailed(reason); + }); + connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) + { + setProgress(current, total); + }); + setStatus(tr("Downloading mods...")); + m_filesNetJob->start(); +} diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 365c3dc4..317562d9 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -47,8 +47,9 @@ protected: private: void processZipPack(); void processMultiMC(); - void processFlame(); void processTechnic(); + void processFlame(); + void processModrinth(); private slots: void downloadSucceeded(); @@ -69,7 +70,8 @@ private: /* data */ enum class ModpackType{ Unknown, MultiMC, + Technic, Flame, - Technic + Modrinth, } m_modpackType = ModpackType::Unknown; }; diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp new file mode 100644 index 00000000..2100aaf9 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -0,0 +1,16 @@ +/* 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" diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h new file mode 100644 index 00000000..9742aeb2 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -0,0 +1,32 @@ +/* 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 + +namespace Modrinth { +struct File +{ + QString path; + QCryptographicHash::Algorithm hashAlgorithm; + QByteArray hash; + // TODO: should this support multiple download URLs, like the JSON does? + QUrl download; +}; +} diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 0fe673ff..1671093d 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -20,6 +20,9 @@ scalable/atlauncher.svg scalable/atlauncher-placeholder.png + + scalable/modrinth.svg + scalable/proxy.svg diff --git a/launcher/resources/multimc/scalable/modrinth.svg b/launcher/resources/multimc/scalable/modrinth.svg new file mode 100644 index 00000000..32715f5c --- /dev/null +++ b/launcher/resources/multimc/scalable/modrinth.svg @@ -0,0 +1,4 @@ + + + + diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index b402839c..05ea091d 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -39,6 +39,7 @@ #include "ui/pages/modplatform/legacy_ftb/Page.h" #include "ui/pages/modplatform/flame/FlamePage.h" #include "ui/pages/modplatform/ImportPage.h" +#include "ui/pages/modplatform/modrinth/ModrinthPage.h" #include "ui/pages/modplatform/technic/TechnicPage.h" @@ -134,6 +135,7 @@ QList NewInstanceDialog::getPages() flamePage, new FtbPage(this), new LegacyFTB::Page(this), + new ModrinthPage(this), technicPage }; } diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index 1b53dd40..8ae38f8d 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -109,7 +109,8 @@ void ImportPage::updateState() { // FIXME: actually do some validation of what's inside here... this is fake AF QFileInfo fi(input); - if(fi.exists() && fi.suffix() == "zip") + // mrpack is a modrinth pack + if(fi.exists() && (fi.suffix() == "zip" || fi.suffix() == "mrpack")) { QFileInfo fi(url.fileName()); dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); @@ -143,6 +144,7 @@ void ImportPage::setUrl(const QString& url) void ImportPage::on_modpackBtn_clicked() { + // TODO: Add .mrpack filter auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); if (url.isValid()) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp new file mode 100644 index 00000000..93b1ca02 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-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 "ModrinthPage.h" + +#include "ui_ModrinthPage.h" + +#include + +ModrinthPage::ModrinthPage(NewInstanceDialog *dialog, QWidget *parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog) +{ + ui->setupUi(this); +} + +ModrinthPage::~ModrinthPage() +{ + delete ui; +} + +void ModrinthPage::openedImpl() +{ + BasePage::openedImpl(); + triggerSearch(); +} + +bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + auto *keyEvent = reinterpret_cast(event); + if (keyEvent->key() == Qt::Key_Return) { + this->triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QObject::eventFilter(watched, event); +} + +void ModrinthPage::triggerSearch() { + +} diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h new file mode 100644 index 00000000..6c75b60d --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -0,0 +1,62 @@ +/* + * Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-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 "Application.h" +#include "ui/dialogs/NewInstanceDialog.h" +#include "ui/pages/BasePage.h" + +#include + +namespace Ui +{ + class ModrinthPage; +} + +class ModrinthPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit ModrinthPage(NewInstanceDialog *dialog, QWidget *parent = nullptr); + ~ModrinthPage() override; + + QString displayName() const override + { + return tr("Modrinth"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("modrinth"); + } + QString id() const override + { + return "modrinth"; + } + + void openedImpl() override; + + bool eventFilter(QObject *watched, QEvent *event) override; + +private slots: + void triggerSearch(); + +private: + Ui::ModrinthPage *ui; + NewInstanceDialog *dialog; +}; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui new file mode 100644 index 00000000..7ef099d3 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -0,0 +1,94 @@ + + + ModrinthPage + + + + 0 + 0 + 837 + 685 + + + + + + + + + Search and filter ... + + + + + + + Search + + + + + + + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + + + true + + + true + + + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + searchEdit + searchButton + packView + packDescription + sortByBox + versionSelectionBox + + + + From 31988f0529f6c316d6a9ba3e66cf981a807ed710 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 14 May 2022 19:56:38 +0200 Subject: [PATCH 374/605] fix: adapt upstream Modrinth code to our codebase --- launcher/CMakeLists.txt | 8 +-- launcher/InstanceImportTask.cpp | 2 - .../multimc/128x128/instances/modrinth.png | Bin 10575 -> 0 bytes .../multimc/32x32/instances/modrinth.png | Bin 1913 -> 0 bytes launcher/resources/multimc/multimc.qrc | 3 -- launcher/ui/pages/modplatform/ImportPage.cpp | 2 +- .../modplatform/modrinth/ModrinthPage.cpp | 45 ++++++++++++----- .../pages/modplatform/modrinth/ModrinthPage.h | 46 +++++++++++++----- 8 files changed, 72 insertions(+), 34 deletions(-) delete mode 100644 launcher/resources/multimc/128x128/instances/modrinth.png delete mode 100644 launcher/resources/multimc/32x32/instances/modrinth.png diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index cbe135e2..7984d3c9 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -532,6 +532,8 @@ set(FLAME_SOURCES set(MODRINTH_SOURCES modplatform/modrinth/ModrinthPackIndex.cpp modplatform/modrinth/ModrinthPackIndex.h + modplatform/modrinth/ModrinthPackManifest.cpp + modplatform/modrinth/ModrinthPackManifest.h ) set(MODPACKSCH_SOURCES @@ -563,11 +565,6 @@ set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLShareCode.h ) -set(MODRINTH_SOURCES - modplatform/modrinth/ModrinthPackManifest.cpp - modplatform/modrinth/ModrinthPackManifest.h -) - add_unit_test(Index SOURCES meta/Index_test.cpp LIBS Launcher_logic @@ -601,7 +598,6 @@ set(LOGIC_SOURCES ${MODPACKSCH_SOURCES} ${TECHNIC_SOURCES} ${ATLAUNCHER_SOURCES} - ${MODRINTH_SOURCES} ) SET(LAUNCHER_SOURCES diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 51715581..ec0f58e0 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -581,8 +581,6 @@ void InstanceImportTask::processModrinth() { QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(configPath); - instanceSettings->registerSetting("InstanceType", "Legacy"); - instanceSettings->set("InstanceType", "OneSix"); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); auto components = instance.getPackProfile(); components->buildingFromScratch(); diff --git a/launcher/resources/multimc/128x128/instances/modrinth.png b/launcher/resources/multimc/128x128/instances/modrinth.png deleted file mode 100644 index 740bc8f02469f108db79d92d05484aff17bf8801..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10575 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 diff --git a/launcher/resources/multimc/32x32/instances/modrinth.png b/launcher/resources/multimc/32x32/instances/modrinth.png deleted file mode 100644 index 025ed06534234458f6de2456868d0a66b4324f3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1913 zcmV-<2Zs2GP)3&289m><@6E99O-OKpvFD)x&4M7Lr726LBnr*iX251# zY=ak;1huGYRobdkkfK&qNs2(JcO2sdCV_^irMOE#t=fh*Qq(0R_ShaEPN9Gj2W$^B z@7>cMGh@bV32pwh_t(7p&Uepw-}l`+=K}xhLv92d+zYKg4BTbevcLedXgCD~m6EfB zIhIVQqjTQt{A)N2^N9*5teQCmezRUHStCkrzaG{Y#z@wnKk-7#`V;T}w*sW7F@FP; z-XemDnMso8VDNV+ISo+J$gQIIX%QKhg*i*tY;G>?s`-xu1b5CE;~SIQtu!7_FW7Gk zYLPHa$?;upMwY`Hh7Zs2>1hM^DZegLA_XGi13=>;k~lX|64~_b;Kvpa+&O2Ae_ZmA z2o{2*F?iV|+^1X1drl7nhM^D!0Wf=df!6xPhRS!rwm02;Jq z$P4T8?pK!edWM{aJv4aEb4^*Kvik%8P)r^GD3J#(ZU1>??aT!L&>M?>l?Hzh5m8z_ z8?4QlbWH(btEWX|JjoADTs8v0it4HbTm#y8&{xJxU}r$MxJb=PkJprAf~p=GOIipW@!?YqCy z`49kv8l8Pg%ag`1=5%-FXAY|&0V`?(pYe@Uew z7ffzjS@&v}j0*APET3rrY~`;74_jA1O5cFxLRt;~;>RUK^r+A_~e!b>cmcNzeI zcq=7cof?WE4z?6WpS^^KH8m%1@qpMz53G6TJPh7~256+exwzvg04!>9iU8yq!zou} zAQ*(6R{=X1gvK>SWOeimDX*oHf`!5Qz^4G9us*OjC(r4%M_9ieYkRA%6%VF+PA>J{ zkO1GZ!1h_6cP{uIarULsc-RHdn+}*=P@jKWsL^>@gbx9Lh)nj|(jIDbnzbzlM9EF5 z?Tf?dm1n1LJc&z3^(_FPM9yS*he`mTGu0@Z4?-Xyj2wYN0E%lYrJfOyQ9!EeA-G9I z?h%c90Dz=xl3dkV5&g%KFf1i_A~laAsS@RmATUAzh~VujG5{BU0szr+Yhmq-J5xla zEB&=oL}V1nJ~zp4SY9$0l$X-P2^#*w^RTLAb@UYgm^R%hgOb|-z)QH1R7PX*(Szgi z=$gmff#jhK?X?b~CJIMsg)adjX(Afed#a_hdmjJU)cQ(|84w8N8FZPVfZ;l!qcej`D9wqU< zh>TaZs`JO4&Y~A*ZVNT!mlxDK_W=M-1tw{Q9T{?ykT15C#$pd|5BM!xS1TnsqEKiv zKFkvg6VO`TbK1E0UMe@Dgz#5azL9KQ)BSr~^cENouK|E4nIQ^aQ?{Vy6J$;q3wcr14Uvv=jq?@vyhKxMRh2dtj)Z z*_I4|A=?viwKh`zPR}r4IGw@Enya8TFxU308VKeAfH6FrI6JuPYQFGBOliW9ZRR2x zyz}wbFJCNJl{a2%eZS>HB?QYvM3W>8U+Rkwlw8dZUROZoeP~x;y=b-dGFv_fqQiLn zmxzccOoJdGB6b$$obk-&%PrWA3IG5+x#Nz>BgVKJKJLXt$QttrT}+0J;9h8TA#k@P zvVaPAWUgk~QfKIS$6RBM&N|uiw;Q>d{eS!$l(>|MSu=Nq00000NkvXXu0mjfgB*o# diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 1671093d..86ebf753 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -275,9 +275,6 @@ 32x32/instances/flame.png 128x128/instances/flame.png - 32x32/instances/modrinth.png - 128x128/instances/modrinth.png - 32x32/instances/gear.png 128x128/instances/gear.png diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index 8ae38f8d..3b65de9d 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -144,8 +144,8 @@ void ImportPage::setUrl(const QString& url) void ImportPage::on_modpackBtn_clicked() { - // TODO: Add .mrpack filter auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); + filter += ";;" + tr("Modrinth pack (*.mrpack)"); const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); if (url.isValid()) { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 93b1ca02..0d65ef16 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -1,18 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2013-2021 MultiMC Contributors - * Copyright 2021-2022 kb1000 + * PolyMC - Minecraft Launcher * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 2021-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 "ModrinthPage.h" @@ -31,6 +49,11 @@ ModrinthPage::~ModrinthPage() delete ui; } +void ModrinthPage::retranslate() +{ + ui->retranslateUi(this); +} + void ModrinthPage::openedImpl() { BasePage::openedImpl(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 6c75b60d..562049b4 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -1,18 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2013-2021 MultiMC Contributors - * Copyright 2021-2022 kb1000 + * PolyMC - Minecraft Launcher * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 2021-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 @@ -49,6 +67,12 @@ public: return "modrinth"; } + virtual QString helpPage() const override + { + return "Modrinth-platform"; + } + void retranslate() override; + void openedImpl() override; bool eventFilter(QObject *watched, QEvent *event) override; From 4fda35b466e4e3f242955cf8cb692a10e8820f0b Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 14 May 2022 20:17:05 -0300 Subject: [PATCH 375/605] feat: add modrinth pack downloading Things that don't work / work poorly (there's more for sure but those are the evident ones): - Icons are broken in the import dialog - No way to search for private packs - Icons are not downloaded when downloading a mod - No support for multiple download URLs - Probably a lot more... --- launcher/CMakeLists.txt | 2 + .../modrinth/ModrinthPackManifest.cpp | 84 ++++++ .../modrinth/ModrinthPackManifest.h | 50 ++++ .../modplatform/modrinth/ModrinthModel.cpp | 274 ++++++++++++++++++ .../modplatform/modrinth/ModrinthModel.h | 81 ++++++ .../modplatform/modrinth/ModrinthPage.cpp | 209 ++++++++++++- .../pages/modplatform/modrinth/ModrinthPage.h | 65 +++-- 7 files changed, 729 insertions(+), 36 deletions(-) create mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp create mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthModel.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 7984d3c9..8e75be20 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -778,6 +778,8 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/modrinth/ModrinthPage.cpp ui/pages/modplatform/modrinth/ModrinthPage.h + ui/pages/modplatform/modrinth/ModrinthModel.cpp + ui/pages/modplatform/modrinth/ModrinthModel.h ui/pages/modplatform/technic/TechnicModel.cpp ui/pages/modplatform/technic/TechnicModel.h diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 2100aaf9..4dcd2fd4 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -14,3 +14,87 @@ */ #include "ModrinthPackManifest.h" +#include "Json.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +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"); + pack.authors << Json::ensureString(obj, "author"); + pack.iconName = QString("modrinth_%1").arg(Json::ensureString(obj, "slug")); + pack.iconUrl = Json::ensureString(obj, "icon_url"); +} + +void loadIndexedInfo(Modpack& pack, QJsonObject& obj) +{ + pack.extra.body = Json::ensureString(obj, "body"); + pack.extra.sourceUrl = Json::ensureString(obj, "source_url"); + pack.extra.wikiUrl = Json::ensureString(obj, "wiki_url"); + + 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"); + + 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); + if (!Json::ensureBoolean(parent, "primary", false)) { + continue; + } + + file.download_url = Json::requireString(parent, "url"); + 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 index 9742aeb2..7dab893c 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -15,18 +15,68 @@ #pragma once +#include + #include #include #include #include +class MinecraftInstance; + namespace Modrinth { + struct File { QString path; + QCryptographicHash::Algorithm hashAlgorithm; QByteArray hash; // TODO: should this support multiple download URLs, like the JSON does? QUrl download; }; + +struct ModpackExtra { + QString body; + + QString sourceUrl; + QString wikiUrl; +}; + +struct ModpackVersion { + QString name; + QString version; + + QString id; + QString project_id; + + QString date; + + QString download_url; +}; + +struct Modpack { + QString id; + + QString name; + QString description; + QStringList authors; + 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; + } + +Q_DECLARE_METATYPE(Modrinth::Modpack); +Q_DECLARE_METATYPE(Modrinth::ModpackVersion); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp new file mode 100644 index 00000000..2890e27d --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -0,0 +1,274 @@ +#include "ModrinthModel.h" + +#include "BuildConfig.h" +#include "Json.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "ui/dialogs/ModDownloadDialog.h" + +#include + +namespace Modrinth { + +ModpackListModel::ModpackListModel(ModrinthPage* parent) : QAbstractListModel(parent), m_parent(parent) {} + +auto ModpackListModel::debugName() const -> QString +{ + return m_parent->debugName(); +} + +/******** Make data requests ********/ + +void ModpackListModel::fetchMore(const QModelIndex& parent) +{ + if (parent.isValid()) + return; + if (nextSearchOffset == 0) { + qWarning() << "fetchMore with 0 offset is wrong..."; + return; + } + performPaginatedSearch(); +} + +auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVariant +{ + int pos = index.row(); + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { + return QString("INVALID INDEX %1").arg(pos); + } + + Modrinth::Modpack pack = modpacks.at(pos); + if (role == Qt::DisplayRole) { + return pack.name; + } else if (role == Qt::ToolTipRole) { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; + } else if (role == Qt::DecorationRole) { + // FIXME: help the icons dont have the same size ;-; + if (m_logoMap.contains(pack.iconName)) { + return (m_logoMap.value(pack.iconName)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ModpackListModel*)this)->requestLogo(pack.iconName, pack.iconUrl.toString()); + return icon; + } else if (role == Qt::UserRole) { + QVariant v; + v.setValue(pack); + return v; + } + + return {}; +} + +/* +void ModpackListModel::requestModVersions(ModPlatform::IndexedPack const& current) +{ + auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); + + m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoader() }); +}*/ + +void ModpackListModel::performPaginatedSearch() +{ + // TODO: Move to standalone API + NetJob* netJob = new NetJob("Modrinth::SearchModpack", APPLICATION->network()); + auto searchAllUrl = QString( + "https://staging-api.modrinth.com/v2/search?" + "query=%1&" + "facets=[[\"project_type:modpack\"]]") + .arg(currentSearchTerm); + + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response)); + + QObject::connect(netJob, &NetJob::succeeded, this, [this] { + QJsonParseError parse_error_all {}; + + QJsonDocument doc_all = QJsonDocument::fromJson(m_all_response, &parse_error_all); + if (parse_error_all.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error_all.offset + << " reason: " << parse_error_all.errorString(); + qWarning() << m_all_response; + return; + } + + searchRequestFinished(doc_all); + }); + QObject::connect(netJob, &NetJob::failed, this, &ModpackListModel::searchRequestFailed); + + jobPtr = netJob; + jobPtr->start(); +} + +void ModpackListModel::refresh() +{ + if (jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + nextSearchOffset = 0; + performPaginatedSearch(); +} + +void ModpackListModel::searchWithTerm(const QString& term, const int sort) +{ + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { + return; + } + + currentSearchTerm = term; + currentSort = sort; + + refresh(); +} + +void ModpackListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) +{ + if (m_logoMap.contains(logo)) { + callback(APPLICATION->metacache() + ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))) + ->getFullPath()); + } else { + requestLogo(logo, logoUrl); + } +} + +void ModpackListModel::requestLogo(QString logo, QString url) +{ + if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { + return; + } + + MetaEntryPtr entry = + APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); + auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + job->deleteLater(); + emit logoLoaded(logo, QIcon(fullPath)); + if (waitingCallbacks.contains(logo)) { + waitingCallbacks.value(logo)(fullPath); + } + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + job->deleteLater(); + emit logoFailed(logo); + }); + + job->start(); + m_loadingLogos.append(logo); +} + +/******** Request callbacks ********/ + +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) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); + } + } +} + +void ModpackListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all) +{ + jobPtr.reset(); + + QList newList; + + auto packs_all = doc_all.object().value("hits").toArray(); + for (auto packRaw : packs_all) { + auto packObj = packRaw.toObject(); + + 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() < 25) { + searchState = Finished; + } else { + nextSearchOffset += 25; + searchState = CanPossiblyFetchMore; + } + + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void ModpackListModel::searchRequestFailed(QString reason) +{ + if (!jobPtr->first()->m_reply) { + // Network error + QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods.")); + } else if (jobPtr->first()->m_reply && jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { + // 409 Gone, notify user to update + QMessageBox::critical(nullptr, tr("Error"), + //: %1 refers to the launcher itself + QString("%1 %2") + .arg(m_parent->displayName()) + .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME))); + } + jobPtr.reset(); + + if (searchState == ResetRequested) { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + nextSearchOffset = 0; + performPaginatedSearch(); + } else { + searchState = Finished; + } +} + +void ModpackListModel::versionRequestSucceeded(QJsonDocument doc, QString id) +{ + auto& current = m_parent->getCurrent(); + if (id != current.id) { + return; + } + + auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); + + try { + // loadIndexedPackVersions(current, arr); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause(); + } + + // m_parent->updateModVersions(); +} + +} // namespace Modrinth + +/******** Helpers ********/ diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h new file mode 100644 index 00000000..1fdbe278 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -0,0 +1,81 @@ +#pragma once + +#include + +#include "modplatform/modrinth/ModrinthPackManifest.h" +#include "ui/pages/modplatform/modrinth/ModrinthPage.h" + +class ModPage; +class Version; + +namespace Modrinth { + +using LogoMap = QMap; +using LogoCallback = std::function; + +class ModpackListModel : public QAbstractListModel { + Q_OBJECT + + public: + ModpackListModel(ModrinthPage* parent); + ~ModpackListModel() override = default; + + inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); }; + inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; }; + inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; + + auto debugName() const -> QString; + + /* Retrieve information from the model at a given index with the given role */ + auto data(const QModelIndex& index, int role) const -> QVariant override; + + inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } + + /* Ask the API for more information */ + void fetchMore(const QModelIndex& parent) override; + void refresh(); + void searchWithTerm(const QString& term, const int sort); + + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); + + inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return searchState == CanPossiblyFetchMore; }; + + public slots: + void searchRequestFinished(QJsonDocument& doc_all); + void searchRequestFailed(QString reason); + + void versionRequestSucceeded(QJsonDocument doc, QString addonId); + + protected slots: + + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + + void performPaginatedSearch(); + + protected: + void requestLogo(QString file, QString url); + + inline auto getMineVersions() const -> std::list; + + protected: + ModrinthPage* m_parent; + + QList modpacks; + + LogoMap m_logoMap; + QMap waitingCallbacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + + QString currentSearchTerm; + int currentSort = 0; + int nextSearchOffset = 0; + enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; + + NetJob::Ptr jobPtr; + + QByteArray m_all_response; + QByteArray m_specific_response; +}; +} // namespace ModPlatform diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 0d65ef16..68805316 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -34,14 +34,41 @@ */ #include "ModrinthPage.h" - #include "ui_ModrinthPage.h" -#include +#include "ModrinthModel.h" -ModrinthPage::ModrinthPage(NewInstanceDialog *dialog, QWidget *parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog) +#include "InstanceImportTask.h" +#include "Json.h" + +#include + +#include +#include +#include + +ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog) { ui->setupUi(this); + + connect(ui->searchButton, &QPushButton::clicked, this, &ModrinthPage::triggerSearch); + ui->searchEdit->installEventFilter(this); + m_model = new Modrinth::ModpackListModel(this); + ui->packView->setModel(m_model); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + 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, &ModrinthPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); } ModrinthPage::~ModrinthPage() @@ -60,10 +87,10 @@ void ModrinthPage::openedImpl() triggerSearch(); } -bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) +bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) { if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { - auto *keyEvent = reinterpret_cast(event); + auto* keyEvent = reinterpret_cast(event); if (keyEvent->key() == Qt::Key_Return) { this->triggerSearch(); keyEvent->accept(); @@ -73,6 +100,176 @@ bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) return QObject::eventFilter(watched, event); } -void ModrinthPage::triggerSearch() { +void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + if (!first.isValid()) { + if (isOpened) { + dialog->setSuggestedPack(); + } + return; + } + + current = m_model->data(first, Qt::UserRole).value(); + auto name = current.name; + + if (!current.extraInfoLoaded) { + qDebug() << "Loading modrinth modpack information"; + + auto netJob = new NetJob(QString("Modrinth::PackInformation(%1)").arg(current.name), APPLICATION->network()); + auto response = new QByteArray(); + + QString id = current.id; + + netJob->addNetAction(Net::Download::makeByteArray(QString("https://staging-api.modrinth.com/v2/project/%1").arg(id), response)); + + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] { + 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 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(); + suggestCurrent(); + }); + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { + netJob->deleteLater(); + delete response; + }); + netJob->start(); + } else + updateUI(); + + if (!current.versionsLoaded) { + qDebug() << "Loading modrinth modpack versions"; + + auto netJob = new NetJob(QString("Modrinth::PackVersions(%1)").arg(current.name), APPLICATION->network()); + auto response = new QByteArray(); + + QString id = current.id; + + netJob->addNetAction( + Net::Download::makeByteArray(QString("https://staging-api.modrinth.com/v2/project/%1/version").arg(id), response)); + + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] { + 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; + } + + try { + Modrinth::loadIndexedVersions(current, doc); + } catch (const JSONValidationError& e) { + qDebug() << *response; + qWarning() << "Error while reading modrinth modpack version: " << e.cause(); + } + + for (auto version : current.versions) { + ui->versionSelectionBox->addItem(version.version, QVariant(version.id)); + } + + updateVersionsUI(); + suggestCurrent(); + }); + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { + netJob->deleteLater(); + delete response; + }); + netJob->start(); + + } else { + for (auto version : current.versions) { + ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.name, version.version), QVariant(version.id)); + } + + suggestCurrent(); + } +} + +void ModrinthPage::updateUI() +{ + QString text = ""; + + if (current.extra.sourceUrl.isEmpty()) + text = current.name; + else + text = "" + current.name + ""; + + if (!current.authors.empty()) { + // TODO: Implement multiple authors with links + text += "
" + tr(" by ") + current.authors.at(0); + } + + text += "
"; + + HoeDown h; + text += h.process(current.extra.body.toUtf8()); + + ui->packDescription->setHtml(text + current.description); +} + +void ModrinthPage::updateVersionsUI() +{ + // idk +} + +void ModrinthPage::suggestCurrent() +{ + if (!isOpened) { + return; + } + + if (selectedVersion.isEmpty()) { + dialog->setSuggestedPack(); + return; + } + + for (auto& ver : current.versions) { + if (ver.id == selectedVersion) { + dialog->setSuggestedPack(current.name, new InstanceImportTask(ver.download_url)); + + break; + } + } +} + +void ModrinthPage::triggerSearch() +{ + m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); +} + +void ModrinthPage::onVersionSelectionChanged(QString data) +{ + if (data.isNull() || data.isEmpty()) { + selectedVersion = ""; + return; + } + selectedVersion = ui->versionSelectionBox->currentData().toString(); + suggestCurrent(); } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 562049b4..f72a5071 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -39,48 +39,53 @@ #include "ui/dialogs/NewInstanceDialog.h" #include "ui/pages/BasePage.h" +#include "modplatform/modrinth/ModrinthPackManifest.h" + #include -namespace Ui -{ - class ModrinthPage; +namespace Ui { +class ModrinthPage; } -class ModrinthPage : public QWidget, public BasePage -{ +namespace Modrinth { +class ModpackListModel; +} + +class ModrinthPage : public QWidget, public BasePage { Q_OBJECT -public: - explicit ModrinthPage(NewInstanceDialog *dialog, QWidget *parent = nullptr); + public: + explicit ModrinthPage(NewInstanceDialog* dialog, QWidget* parent = nullptr); ~ModrinthPage() override; - QString displayName() const override - { - return tr("Modrinth"); - } - QIcon icon() const override - { - return APPLICATION->getThemedIcon("modrinth"); - } - QString id() const override - { - return "modrinth"; - } + QString displayName() const override { return tr("Modrinth"); } + QIcon icon() const override { return APPLICATION->getThemedIcon("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"; }; + + auto getCurrent() -> Modrinth::Modpack& { return current; } + void suggestCurrent(); + + void updateUI(); + void updateVersionsUI(); - virtual QString helpPage() const override - { - return "Modrinth-platform"; - } void retranslate() override; - void openedImpl() override; + bool eventFilter(QObject* watched, QEvent* event) override; - bool eventFilter(QObject *watched, QEvent *event) override; - -private slots: + private slots: + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); void triggerSearch(); -private: - Ui::ModrinthPage *ui; - NewInstanceDialog *dialog; + private: + Ui::ModrinthPage* ui; + NewInstanceDialog* dialog; + Modrinth::ModpackListModel* m_model; + + Modrinth::Modpack current; + QString selectedVersion; }; From 9dd70ca9ae6fdab913a77467e803bf90ddd949ed Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 14 May 2022 20:26:20 -0300 Subject: [PATCH 376/605] fix: download icon as well when importing modrinth modpacks --- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 3 +++ launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui | 3 +++ 2 files changed, 6 insertions(+) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 68805316..b21fdf4a 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -253,6 +253,9 @@ void ModrinthPage::suggestCurrent() for (auto& ver : current.versions) { if (ver.id == selectedVersion) { dialog->setSuggestedPack(current.name, new InstanceImportTask(ver.download_url)); + auto iconName = current.iconName; + m_model->getLogo(iconName, current.iconUrl.toString(), + [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); break; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 7ef099d3..8de53a69 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -45,6 +45,9 @@ 48
+ + true +
From 5ea8cec16f6dfbaeaca56ccf7f9151039a1dd145 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 14 May 2022 21:29:48 -0300 Subject: [PATCH 377/605] fix: make all modrinth modpacks have the same icon size --- .../modplatform/modrinth/ModrinthModel.cpp | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 2890e27d..121f5d4e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -49,9 +49,10 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian } return pack.description; } else if (role == Qt::DecorationRole) { - // FIXME: help the icons dont have the same size ;-; if (m_logoMap.contains(pack.iconName)) { - return (m_logoMap.value(pack.iconName)); + return (m_logoMap.value(pack.iconName) + .pixmap(48, 48) + .scaled(48, 48, Qt::IgnoreAspectRatio, Qt::TransformationMode::SmoothTransformation)); } QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); ((ModpackListModel*)this)->requestLogo(pack.iconName, pack.iconUrl.toString()); @@ -65,14 +66,6 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian return {}; } -/* -void ModpackListModel::requestModVersions(ModPlatform::IndexedPack const& current) -{ - auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); - - m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoader() }); -}*/ - void ModpackListModel::performPaginatedSearch() { // TODO: Move to standalone API @@ -86,7 +79,7 @@ void ModpackListModel::performPaginatedSearch() netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response)); QObject::connect(netJob, &NetJob::succeeded, this, [this] { - QJsonParseError parse_error_all {}; + QJsonParseError parse_error_all{}; QJsonDocument doc_all = QJsonDocument::fromJson(m_all_response, &parse_error_all); if (parse_error_all.error != QJsonParseError::NoError) { @@ -210,7 +203,7 @@ void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all) continue; } } - + if (packs_all.size() < 25) { searchState = Finished; } else { From 9899a0e098e5cfb76a754fa9da2f73be46cc880a Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 14 May 2022 21:47:35 -0300 Subject: [PATCH 378/605] fix: Have the URL be the project URL itself (I think, doesn't seem to work for the waffle though, probably because of the staging API :/) --- launcher/modplatform/modrinth/ModrinthPackManifest.cpp | 1 + launcher/modplatform/modrinth/ModrinthPackManifest.h | 1 + launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 4dcd2fd4..4b8a9a9b 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -35,6 +35,7 @@ void loadIndexedPack(Modpack& pack, QJsonObject& obj) 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.sourceUrl = Json::ensureString(obj, "source_url"); pack.extra.wikiUrl = Json::ensureString(obj, "wiki_url"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 7dab893c..aaaacf2c 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -39,6 +39,7 @@ struct File struct ModpackExtra { QString body; + QString projectUrl; QString sourceUrl; QString wikiUrl; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index b21fdf4a..cf519b8c 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -219,7 +219,7 @@ void ModrinthPage::updateUI() if (current.extra.sourceUrl.isEmpty()) text = current.name; else - text = "" + current.name + ""; + text = "" + current.name + ""; if (!current.authors.empty()) { // TODO: Implement multiple authors with links From 365cc198ba1e4e8129c95291e60e2c3c7ffbbf7a Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 14 May 2022 21:50:54 -0300 Subject: [PATCH 379/605] refactor: some random improvements --- launcher/InstanceImportTask.cpp | 8 ++++---- launcher/ui/pages/modplatform/ImportPage.cpp | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index ec0f58e0..29e3a26c 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -504,16 +504,16 @@ void InstanceImportTask::processModrinth() { QJsonObject hashes = Json::requireObject(obj, "hashes"); QString hash; QCryptographicHash::Algorithm hashAlgorithm; - hash = Json::ensureString(hashes, "sha256"); - hashAlgorithm = QCryptographicHash::Sha256; + hash = Json::ensureString(hashes, "sha1"); + hashAlgorithm = QCryptographicHash::Sha1; if (hash.isEmpty()) { hash = Json::ensureString(hashes, "sha512"); hashAlgorithm = QCryptographicHash::Sha512; if (hash.isEmpty()) { - hash = Json::ensureString(hashes, "sha1"); - hashAlgorithm = QCryptographicHash::Sha1; + hash = Json::ensureString(hashes, "sha256"); + hashAlgorithm = QCryptographicHash::Sha256; if (hash.isEmpty()) { throw JSONValidationError("No hash found for: " + file.path); diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index 3b65de9d..c86d02ca 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -110,7 +110,10 @@ void ImportPage::updateState() // FIXME: actually do some validation of what's inside here... this is fake AF QFileInfo fi(input); // mrpack is a modrinth pack - if(fi.exists() && (fi.suffix() == "zip" || fi.suffix() == "mrpack")) + + // Allow non-latin people to use ZIP files! + auto zip = QMimeDatabase().mimeTypeForUrl(url).suffixes().contains("zip"); + if(fi.exists() && (zip || fi.suffix() == "mrpack")) { QFileInfo fi(url.fileName()); dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); From 49de5d9b07c8e05681ef9d485ccfd3d8e4bca784 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 14 May 2022 22:04:40 -0300 Subject: [PATCH 380/605] change: list what file types can be entered in the importer --- launcher/ui/pages/modplatform/ImportPage.ui | 78 +++++++++++++++++---- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/launcher/ui/pages/modplatform/ImportPage.ui b/launcher/ui/pages/modplatform/ImportPage.ui index eb63cbe9..77bc5da5 100644 --- a/launcher/ui/pages/modplatform/ImportPage.ui +++ b/launcher/ui/pages/modplatform/ImportPage.ui @@ -11,28 +11,75 @@ - - - - Browse - - - - + http:// - - + + - Local file or link to a direct download: + Browse - + + + + + + The following file types are implemented (both for local files and URLs): + + + Qt::AlignCenter + + + + + + + - Curseforge modpacks (ZIP) + + + Qt::AlignCenter + + + + + + + - Modrinth modpacks (ZIP and mrpack) + + + Qt::AlignCenter + + + + + + + - PolyMC / MultiMC exported instances (ZIP) + + + Qt::AlignCenter + + + + + + + - Technic modpacks (ZIP) + + + Qt::AlignCenter + + + + + + Qt::Vertical @@ -45,6 +92,13 @@ + + + + Local file or link to a direct download: + + + From 4745ed28186f46de60de155826c8f2bfb54f45cb Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 14 May 2022 22:12:51 -0300 Subject: [PATCH 381/605] fix: choose valid download url even if it's not the primary one It seems to be possible to have modpack versions that have to primary file. In those cases, we pick a valid one "at random". --- .../modplatform/modrinth/ModrinthPackManifest.cpp | 14 +++++++++++--- .../modplatform/modrinth/ModrinthPackManifest.h | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 4b8a9a9b..88ca808a 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -81,15 +81,23 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion auto files = Json::requireArray(obj, "files"); + qWarning() << files; + for (auto file_iter : files) { File indexed_file; auto parent = Json::requireObject(file_iter); - if (!Json::ensureBoolean(parent, "primary", false)) { - continue; + auto is_primary = Json::ensureBoolean(parent, "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; } file.download_url = Json::requireString(parent, "url"); - break; + if(is_primary) + break; } if(file.download_url.isEmpty()) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index aaaacf2c..585f692a 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -79,5 +79,5 @@ auto loadIndexedVersion(QJsonObject&) -> ModpackVersion; } -Q_DECLARE_METATYPE(Modrinth::Modpack); -Q_DECLARE_METATYPE(Modrinth::ModpackVersion); +Q_DECLARE_METATYPE(Modrinth::Modpack) +Q_DECLARE_METATYPE(Modrinth::ModpackVersion) From 9731e06728ab1bdf11f6891b563d9f7123c1a0d8 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 15 May 2022 11:49:27 +0200 Subject: [PATCH 382/605] fix: fix build on Qt 5.12 --- launcher/modplatform/modrinth/ModrinthPackManifest.h | 1 + 1 file changed, 1 insertion(+) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 585f692a..33c3fc5e 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -21,6 +21,7 @@ #include #include #include +#include class MinecraftInstance; From a43f882d482061b86a339c1338e26246f6fc5f70 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 15 May 2022 12:06:01 +0200 Subject: [PATCH 383/605] feat: add support for Quilt Loader in Modrinth packs --- launcher/InstanceImportTask.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 29e3a26c..29310538 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -480,7 +480,7 @@ void InstanceImportTask::processMultiMC() void InstanceImportTask::processModrinth() { std::vector files; - QString minecraftVersion, fabricVersion, forgeVersion; + QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion; try { QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); @@ -547,6 +547,12 @@ void InstanceImportTask::processModrinth() { throw JSONValidationError("Duplicate Fabric Loader version"); fabricVersion = Json::requireString(*it, "Fabric Loader version"); } + else if (name == "quilt-loader") + { + if (!quiltVersion.isEmpty()) + throw JSONValidationError("Duplicate Quilt Loader version"); + quiltVersion = Json::requireString(*it, "Quilt Loader version"); + } else if (name == "forge") { if (!forgeVersion.isEmpty()) @@ -587,6 +593,8 @@ void InstanceImportTask::processModrinth() { components->setComponentVersion("net.minecraft", minecraftVersion, true); if (!fabricVersion.isEmpty()) components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion, true); + if (!quiltVersion.isEmpty()) + components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion, true); if (!forgeVersion.isEmpty()) components->setComponentVersion("net.minecraftforge", forgeVersion, true); if (m_instIcon != "default") From 4a0e4fdb85ae6782406919c4b4df9554a81356aa Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 07:12:31 -0300 Subject: [PATCH 384/605] fix: add author page url --- launcher/modplatform/modrinth/ModrinthPackManifest.cpp | 7 ++++++- launcher/modplatform/modrinth/ModrinthPackManifest.h | 2 +- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 8 +++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 88ca808a..f690984b 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -16,9 +16,13 @@ #include "ModrinthPackManifest.h" #include "Json.h" +#include "modplatform/modrinth/ModrinthAPI.h" + #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +static ModrinthAPI api; + namespace Modrinth { void loadIndexedPack(Modpack& pack, QJsonObject& obj) @@ -27,7 +31,8 @@ void loadIndexedPack(Modpack& pack, QJsonObject& obj) pack.name = Json::ensureString(obj, "title"); pack.description = Json::ensureString(obj, "description"); - pack.authors << Json::ensureString(obj, "author"); + auto temp_author_name = Json::ensureString(obj, "author"); + pack.author = std::make_tuple(temp_author_name, api.getAuthorURL(temp_author_name)); pack.iconName = QString("modrinth_%1").arg(Json::ensureString(obj, "slug")); pack.iconUrl = Json::ensureString(obj, "icon_url"); } diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 33c3fc5e..47817bad 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -62,7 +62,7 @@ struct Modpack { QString name; QString description; - QStringList authors; + std::tuple author; QString iconName; QUrl iconUrl; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index cf519b8c..acfd14b5 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -221,10 +221,8 @@ void ModrinthPage::updateUI() else text = "" + current.name + ""; - if (!current.authors.empty()) { - // TODO: Implement multiple authors with links - text += "
" + tr(" by ") + current.authors.at(0); - } + // TODO: Implement multiple authors with links + text += "
" + tr(" by ") + QString("%2").arg(std::get<1>(current.author).toString(), std::get<0>(current.author)); text += "
"; @@ -255,7 +253,7 @@ void ModrinthPage::suggestCurrent() dialog->setSuggestedPack(current.name, new InstanceImportTask(ver.download_url)); auto iconName = current.iconName; m_model->getLogo(iconName, current.iconUrl.toString(), - [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); + [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); break; } From 4bb429a0fbe698d0f4dbdbf02719e76730b5b6bd Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 07:43:02 -0300 Subject: [PATCH 385/605] change: use build variables for the modrinth API URLs Make it more consistent with the others --- buildconfig/BuildConfig.h | 3 +++ launcher/modplatform/modrinth/ModrinthAPI.h | 22 ++++++++++--------- .../modplatform/modrinth/ModrinthModel.cpp | 6 ++--- .../modplatform/modrinth/ModrinthPage.cpp | 5 +++-- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index a920a3d4..8594e46d 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -151,6 +151,9 @@ class Config { */ QString TECHNIC_API_BUILD = "multimc"; + QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2"; + QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2"; + /** * \brief Converts the Version to a string. * \return The version number in string format (major.minor.revision.build). diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 86852c94..87438375 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -1,5 +1,6 @@ #pragma once +#include "BuildConfig.h" #include "modplatform/ModAPI.h" #include "modplatform/helpers/NetworkModAPI.h" @@ -47,13 +48,13 @@ class ModrinthAPI : public NetworkModAPI { return ""; } - return QString( - "https://api.modrinth.com/v2/search?" - "offset=%1&" - "limit=25&" - "query=%2&" - "index=%3&" - "facets=[[%4],%5[\"project_type:mod\"]]") + return QString(BuildConfig.MODRINTH_PROD_URL + + "/search?" + "offset=%1&" + "limit=25&" + "query=%2&" + "index=%3&" + "facets=[[%4],%5[\"project_type:mod\"]]") .arg(args.offset) .arg(args.search) .arg(args.sorting) @@ -63,9 +64,10 @@ class ModrinthAPI : public NetworkModAPI { inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { - return QString("https://api.modrinth.com/v2/project/%1/version?" - "game_versions=[%2]" - "loaders=[\"%3\"]") + return QString(BuildConfig.MODRINTH_PROD_URL + + "/project/%1/version?" + "game_versions=[%2]" + "loaders=[\"%3\"]") .arg(args.addonId) .arg(getGameVersionsString(args.mcVersions)) .arg(getModLoaderStrings(args.loader).join("\",\"")); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 121f5d4e..1d1b4c8e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -71,10 +71,10 @@ void ModpackListModel::performPaginatedSearch() // TODO: Move to standalone API NetJob* netJob = new NetJob("Modrinth::SearchModpack", APPLICATION->network()); auto searchAllUrl = QString( - "https://staging-api.modrinth.com/v2/search?" - "query=%1&" + "%1/search?" + "query=%2&" "facets=[[\"project_type:modpack\"]]") - .arg(currentSearchTerm); + .arg(BuildConfig.MODRINTH_STAGING_URL, currentSearchTerm); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response)); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index acfd14b5..5dc66e56 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -38,6 +38,7 @@ #include "ModrinthModel.h" +#include "BuildConfig.h" #include "InstanceImportTask.h" #include "Json.h" @@ -122,7 +123,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) QString id = current.id; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://staging-api.modrinth.com/v2/project/%1").arg(id), response)); + netJob->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2").arg(BuildConfig.MODRINTH_STAGING_URL, id), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] { if (id != current.id) { @@ -167,7 +168,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) QString id = current.id; netJob->addNetAction( - Net::Download::makeByteArray(QString("https://staging-api.modrinth.com/v2/project/%1/version").arg(id), response)); + Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_STAGING_URL, id), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] { if (id != current.id) { From 3abf466632588f9285579a9822b5da2c9fea7bec Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 15 May 2022 13:20:05 +0200 Subject: [PATCH 386/605] chore: add/update license headers --- launcher/InstanceImportTask.cpp | 40 ++++++++++++++----- launcher/InstanceImportTask.h | 40 ++++++++++++++----- launcher/modplatform/modrinth/ModrinthAPI.h | 17 ++++++++ .../modrinth/ModrinthPackIndex.cpp | 17 ++++++++ .../modplatform/modrinth/ModrinthPackIndex.h | 17 ++++++++ .../modrinth/ModrinthPackManifest.cpp | 40 ++++++++++++++----- .../modrinth/ModrinthPackManifest.h | 40 ++++++++++++++----- launcher/ui/pages/modplatform/ImportPage.cpp | 1 + .../modplatform/modrinth/ModrinthModModel.h | 18 +++++++++ .../modplatform/modrinth/ModrinthModel.cpp | 34 ++++++++++++++++ .../modplatform/modrinth/ModrinthModel.h | 34 ++++++++++++++++ 11 files changed, 258 insertions(+), 40 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 29310538..8a0432c9 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "InstanceImportTask.h" diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 317562d9..0dc6ba88 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 87438375..09eefcd1 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -1,3 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 . + */ + #pragma once #include "BuildConfig.h" diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index a3c2f166..f7fa9864 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -1,3 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 . + */ + #include "ModrinthPackIndex.h" #include "ModrinthAPI.h" diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index fd17847a..7f306f25 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -1,3 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 . + */ + #pragma once #include "modplatform/ModIndex.h" diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index f690984b..f77baa6a 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -1,16 +1,36 @@ -/* Copyright 2022 kb1000 +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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" diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 47817bad..d350477b 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -1,16 +1,36 @@ -/* Copyright 2022 kb1000 +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index c86d02ca..c7bc13d8 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h index 63c23bbe..ae7b0bdd 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h @@ -1,3 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #pragma once #include "ModrinthModPage.h" diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 1d1b4c8e..b0dfb1b7 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 "ModrinthModel.h" #include "BuildConfig.h" diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 1fdbe278..6ec3bb97 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 From 5f2398fe59b0053d94c0600f54bddc642751bf74 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 08:25:58 -0300 Subject: [PATCH 387/605] chore: license headers 2 --- launcher/InstanceImportTask.cpp | 1 + launcher/modplatform/modrinth/ModrinthAPI.h | 1 + launcher/modplatform/modrinth/ModrinthPackManifest.cpp | 1 + launcher/modplatform/modrinth/ModrinthPackManifest.h | 1 + launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp | 1 + launcher/ui/pages/modplatform/modrinth/ModrinthModel.h | 1 + launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 1 + launcher/ui/pages/modplatform/modrinth/ModrinthPage.h | 1 + 8 files changed, 8 insertions(+) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 8a0432c9..26d46be0 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * 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 diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 09eefcd1..6d642b5e 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - 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 diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index f77baa6a..facf5ddb 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - 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 diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index d350477b..55ad40d9 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - 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 diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index b0dfb1b7..50974e13 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - 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 diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 6ec3bb97..e61eae7c 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - 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 diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 5dc66e56..f69983ee 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - 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 diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index f72a5071..9aa702f9 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - 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 From 682a7fb6bad2c2d07ae5ddf67c139ac3f15672bb Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 15 May 2022 13:36:55 +0200 Subject: [PATCH 388/605] feat: add version of Modrinth modpack to instance name --- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index f69983ee..fd9adc24 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -252,7 +252,7 @@ void ModrinthPage::suggestCurrent() for (auto& ver : current.versions) { if (ver.id == selectedVersion) { - dialog->setSuggestedPack(current.name, new InstanceImportTask(ver.download_url)); + dialog->setSuggestedPack(current.name + " " + ver.version, new InstanceImportTask(ver.download_url)); auto iconName = current.iconName; m_model->getLogo(iconName, current.iconUrl.toString(), [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); From 93e0041d0e6c3d7859f7d8b058a0fd014329bec6 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 11:09:45 -0300 Subject: [PATCH 389/605] change: use modrinth icon as default on modrinth packs --- launcher/InstanceImportTask.cpp | 4 ++++ launcher/resources/multimc/multimc.qrc | 2 +- .../resources/multimc/scalable/{ => instances}/modrinth.svg | 0 3 files changed, 5 insertions(+), 1 deletion(-) rename launcher/resources/multimc/scalable/{ => instances}/modrinth.svg (100%) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 26d46be0..f02aed91 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -622,6 +622,10 @@ void InstanceImportTask::processModrinth() { { instance.setIconKey(m_instIcon); } + else + { + instance.setIconKey("modrinth"); + } instance.setName(m_instName); instance.saveNow(); diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 86ebf753..e22fe7ee 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -21,7 +21,7 @@ scalable/atlauncher-placeholder.png - scalable/modrinth.svg + scalable/instances/modrinth.svg scalable/proxy.svg diff --git a/launcher/resources/multimc/scalable/modrinth.svg b/launcher/resources/multimc/scalable/instances/modrinth.svg similarity index 100% rename from launcher/resources/multimc/scalable/modrinth.svg rename to launcher/resources/multimc/scalable/instances/modrinth.svg From 4adc61bda91bb01e603fb975b05651df7decaf52 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 11:26:15 -0300 Subject: [PATCH 390/605] change: update modrinth icon Updates to the version at https://github.com/modrinth/docs/blob/master/static/img/logo.svg --- launcher/resources/multimc/scalable/instances/modrinth.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/resources/multimc/scalable/instances/modrinth.svg b/launcher/resources/multimc/scalable/instances/modrinth.svg index 32715f5c..a40f0e72 100644 --- a/launcher/resources/multimc/scalable/instances/modrinth.svg +++ b/launcher/resources/multimc/scalable/instances/modrinth.svg @@ -1,4 +1,4 @@ - - + + From 78cf0c73c89f0d1207bb079bf4670cc032607c4d Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 15 May 2022 20:38:27 +0200 Subject: [PATCH 391/605] fix: always show project url, if available --- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index fd9adc24..a2e18d19 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -218,7 +218,7 @@ void ModrinthPage::updateUI() { QString text = ""; - if (current.extra.sourceUrl.isEmpty()) + if (current.extra.projectUrl.isEmpty()) text = current.name; else text = "" + current.name + ""; From 7194bb1b8114a2ec96d3cb30a4fe3338f3962d4c Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 15:58:23 -0300 Subject: [PATCH 392/605] fix: validate whitelisted download urls --- launcher/InstanceImportTask.cpp | 2 +- .../modrinth/ModrinthPackManifest.cpp | 25 +++++++++++++++++-- .../modrinth/ModrinthPackManifest.h | 2 ++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index f02aed91..3ca82923 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -545,7 +545,7 @@ void InstanceImportTask::processModrinth() { file.hashAlgorithm = hashAlgorithm; // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly handle spaces) file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); - if (!file.download.isValid()) + if (!file.download.isValid() || !Modrinth::validadeDownloadUrl(file.download)) { throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); } diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index facf5ddb..947ac182 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -93,6 +93,23 @@ void loadIndexedVersions(Modpack& pack, QJsonDocument& doc) pack.versionsLoaded = true; } +auto validadeDownloadUrl(QUrl url) -> bool +{ + auto domain = url.host(); + if(domain == "cdn.modrinth.com") + return true; + if(domain == "edge.forgecdn.net") + return true; + if(domain == "media.forgecdn.net") + return true; + if(domain == "github.com") + return true; + if(domain == "raw.githubusercontent.com") + return true; + + return false; +} + auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion { ModpackVersion file; @@ -107,7 +124,6 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion auto files = Json::requireArray(obj, "files"); - qWarning() << files; for (auto file_iter : files) { File indexed_file; @@ -121,7 +137,12 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion continue; } - file.download_url = Json::requireString(parent, "url"); + auto url = Json::requireString(parent, "url"); + + if(!validadeDownloadUrl(url)) + continue; + + file.download_url = url; if(is_primary) break; } diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 55ad40d9..4db4a75d 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -99,6 +99,8 @@ void loadIndexedInfo(Modpack&, QJsonObject&); void loadIndexedVersions(Modpack&, QJsonDocument&); auto loadIndexedVersion(QJsonObject&) -> ModpackVersion; +auto validadeDownloadUrl(QUrl) -> bool; + } Q_DECLARE_METATYPE(Modrinth::Modpack) From 80908efdcb1f8d4deb35c0df65651cc96fae71ac Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 15 May 2022 16:33:52 -0400 Subject: [PATCH 393/605] Fix indentation of macOS resources --- cmake/MacOSXBundleInfo.plist.in | 8 ++++---- program_info/App.entitlements | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index 0e3a43c6..9e663d31 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -2,10 +2,10 @@ - NSCameraUsageDescription - A Minecraft mod wants to access your camera. - NSMicrophoneUsageDescription - A Minecraft mod wants to access your microphone. + NSCameraUsageDescription + A Minecraft mod wants to access your camera. + NSMicrophoneUsageDescription + A Minecraft mod wants to access your microphone. NSPrincipalClass NSApplication NSHighResolutionCapable diff --git a/program_info/App.entitlements b/program_info/App.entitlements index 1850b990..032308a1 100644 --- a/program_info/App.entitlements +++ b/program_info/App.entitlements @@ -2,11 +2,11 @@ - com.apple.security.cs.disable-library-validation - - com.apple.security.device.audio-input - - com.apple.security.device.camera - + com.apple.security.cs.disable-library-validation + + com.apple.security.device.audio-input + + com.apple.security.device.camera + From 7f305aad1b80e28d15dda71ba62bec2e8eb9dac3 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 15 May 2022 16:34:53 -0400 Subject: [PATCH 394/605] Add Allow DYLD Environment Variables Entitlement to macOS build This allows the Steam overlay to be injected into Minecraft. --- program_info/App.entitlements | 2 ++ 1 file changed, 2 insertions(+) diff --git a/program_info/App.entitlements b/program_info/App.entitlements index 032308a1..b46e8ff2 100644 --- a/program_info/App.entitlements +++ b/program_info/App.entitlements @@ -4,6 +4,8 @@ com.apple.security.cs.disable-library-validation + com.apple.security.cs.allow-dyld-environment-variables + com.apple.security.device.audio-input com.apple.security.device.camera From a110d445ac48a2493bafef8c4ce59a234d0e648e Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 15 May 2022 23:00:09 +0200 Subject: [PATCH 395/605] feat: support quilt.mod.json metadata --- launcher/minecraft/mod/LocalModParseTask.cpp | 53 +++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/LocalModParseTask.cpp index f01da8ae..699cf7ee 100644 --- a/launcher/minecraft/mod/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/LocalModParseTask.cpp @@ -8,6 +8,7 @@ #include #include +#include "Json.h" #include "settings/INIFile.h" #include "FileSystem.h" @@ -262,6 +263,43 @@ std::shared_ptr ReadFabricModInfo(QByteArray contents) return details; } +// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md#the-schema_version-field +std::shared_ptr 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"); + + std::shared_ptr details = std::make_shared(); + + if (schemaVersion == 1) + { + auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info"); + + details->mod_id = Json::requireString(modInfo.value("id"), "Mod ID"); + details->version = Json::requireString(modInfo.value("version"), "Mod version"); + + auto modMetadata = Json::ensureObject(modInfo.value("metadata")); + + details->name = Json::ensureString(modMetadata.value("name"), details->mod_id); + details->description = Json::ensureString(modMetadata.value("description")); + + auto modContributors = Json::ensureObject(modMetadata.value("contributors")); + + // We don't really care about the role of a contributor here + details->authors += modContributors.keys(); + + auto modContact = Json::ensureObject(modMetadata.value("contact")); + + if (modContact.contains("homepage")) + { + details->homeurl = Json::requireString(modContact.value("homepage")); + } + } + return details; +} + std::shared_ptr ReadForgeInfo(QByteArray contents) { std::shared_ptr details = std::make_shared(); @@ -391,7 +429,7 @@ void LocalModParseTask::processAsZip() zip.close(); return; } - else if (zip.setCurrentFile("fabric.mod.json")) // TODO: Support quilt.mod.json + else if (zip.setCurrentFile("fabric.mod.json")) { if (!file.open(QIODevice::ReadOnly)) { @@ -404,6 +442,19 @@ void LocalModParseTask::processAsZip() zip.close(); return; } + else if (zip.setCurrentFile("quilt.mod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadQuiltModInfo(file.readAll()); + file.close(); + zip.close(); + return; + } else if (zip.setCurrentFile("forgeversion.properties")) { if (!file.open(QIODevice::ReadOnly)) From 66ce5a4a2d38803bf667d7326f2ffb1bc06bff99 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 20:45:27 -0300 Subject: [PATCH 396/605] fix: pack sorting and other search parameters --- .../modplatform/modrinth/ModrinthModel.cpp | 24 ++++++++++++++----- .../modplatform/modrinth/ModrinthModel.h | 2 +- .../modplatform/modrinth/ModrinthPage.cpp | 9 ++++--- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 50974e13..6786b0da 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -105,11 +105,16 @@ void ModpackListModel::performPaginatedSearch() { // TODO: Move to standalone API NetJob* netJob = new NetJob("Modrinth::SearchModpack", APPLICATION->network()); - auto searchAllUrl = QString( - "%1/search?" + auto searchAllUrl = QString(BuildConfig.MODRINTH_STAGING_URL + + "/search?" + "offset=%1&" + "limit=20&" "query=%2&" + "index=%3&" "facets=[[\"project_type:modpack\"]]") - .arg(BuildConfig.MODRINTH_STAGING_URL, currentSearchTerm); + .arg(nextSearchOffset) + .arg(currentSearchTerm) + .arg(currentSort); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response)); @@ -148,14 +153,21 @@ void ModpackListModel::refresh() performPaginatedSearch(); } +static std::array sorts {"relevance", "downloads", "follows", "newest", "updated"}; + void ModpackListModel::searchWithTerm(const QString& term, const int sort) { - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { + if(sort > 5 || sort < 0) + return; + + auto sort_str = sorts.at(sort); + + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str) { return; } currentSearchTerm = term; - currentSort = sort; + currentSort = sort_str; refresh(); } @@ -255,7 +267,7 @@ void ModpackListModel::searchRequestFailed(QString reason) { if (!jobPtr->first()->m_reply) { // Network error - QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods.")); + QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks.")); } else if (jobPtr->first()->m_reply && jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { // 409 Gone, notify user to update QMessageBox::critical(nullptr, tr("Error"), diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index e61eae7c..bffea54d 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -104,7 +104,7 @@ class ModpackListModel : public QAbstractListModel { QStringList m_loadingLogos; QString currentSearchTerm; - int currentSort = 0; + QString currentSort; int nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index a2e18d19..ceddcfb5 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -61,12 +61,11 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); - 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 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")); connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); From ec3c882a44624f18b088322b28efe7153e7db083 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 20:52:57 -0300 Subject: [PATCH 397/605] change: add alpha note to modrinth page --- .../ui/pages/modplatform/modrinth/ModrinthPage.ui | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 8de53a69..90e8dba3 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -11,6 +11,21 @@ + + + + + true + + + + Note: Modrinth modpacks is still in alpha phase. Some things may be rough on the edges, or not working at all! Use it with caution. + + + Qt::AlignCenter + + + From e7bb3b277647a21b85cb01ee90bf640e22d01552 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 20:59:07 -0300 Subject: [PATCH 398/605] fix: macos compilation i forgor macos is cringe with static arrays :skull: edit: WHY DONT MAC LET ME USE STD::ARRAY ;----; --- .../modplatform/modrinth/ModrinthModel.cpp | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 6786b0da..2504b294 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -153,14 +153,31 @@ void ModpackListModel::refresh() performPaginatedSearch(); } -static std::array sorts {"relevance", "downloads", "follows", "newest", "updated"}; +static auto sortFromIndex(int index) -> QString +{ + switch(index){ + default: + case 1: + return "relevance"; + case 2: + return "downloads"; + case 3: + return "follows"; + case 4: + return "newest"; + case 5: + return "updated"; + } + + return {}; +} void ModpackListModel::searchWithTerm(const QString& term, const int sort) { if(sort > 5 || sort < 0) return; - auto sort_str = sorts.at(sort); + auto sort_str = sortFromIndex(sort); if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str) { return; From e92b7bd25e9eccf293ff97652364def63a674df2 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 21:50:42 -0300 Subject: [PATCH 399/605] change: switch to modrinth production servers --- launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp | 2 +- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 2504b294..0cf53659 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -105,7 +105,7 @@ void ModpackListModel::performPaginatedSearch() { // TODO: Move to standalone API NetJob* netJob = new NetJob("Modrinth::SearchModpack", APPLICATION->network()); - auto searchAllUrl = QString(BuildConfig.MODRINTH_STAGING_URL + + auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL + "/search?" "offset=%1&" "limit=20&" diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index ceddcfb5..fadad9df 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -123,7 +123,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) QString id = current.id; - netJob->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2").arg(BuildConfig.MODRINTH_STAGING_URL, id), response)); + netJob->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] { if (id != current.id) { @@ -168,7 +168,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) QString id = current.id; netJob->addNetAction( - Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_STAGING_URL, id), response)); + Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] { if (id != current.id) { From 62e099ace5db8e04bba684e6c2517291dcce3578 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 22:16:52 -0300 Subject: [PATCH 400/605] feat: better handling of optional mods This disables the optional mods by default and tell the user about it. Pretty hackish, but a better solution would involve the modrinth metadata to have the mod names... Also sorry for the diffs, my clangd went rogue x.x --- launcher/InstanceImportTask.cpp | 153 +++++++++--------- launcher/InstanceImportTask.h | 5 +- .../modplatform/modrinth/ModrinthPage.cpp | 2 +- 3 files changed, 80 insertions(+), 80 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 3ca82923..64f2dd02 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -35,35 +35,38 @@ */ #include "InstanceImportTask.h" +#include +#include "Application.h" #include "BaseInstance.h" #include "FileSystem.h" -#include "Application.h" #include "MMCZip.h" #include "NullInstance.h" -#include "settings/INISettingsObject.h" #include "icons/IconUtils.h" -#include +#include "settings/INISettingsObject.h" // FIXME: this does not belong here, it's Minecraft/Flame specific +#include +#include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/PackManifest.h" -#include "Json.h" -#include #include "modplatform/modrinth/ModrinthPackManifest.h" #include "modplatform/technic/TechnicPackProcessor.h" -#include "icons/IconList.h" #include "Application.h" +#include "icons/IconList.h" #include "net/ChecksumValidator.h" +#include "ui/dialogs/CustomMessageBox.h" + #include #include -InstanceImportTask::InstanceImportTask(const QUrl sourceUrl) +InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent) { m_sourceUrl = sourceUrl; + m_parent = parent; } bool InstanceImportTask::abort() @@ -476,124 +479,118 @@ void InstanceImportTask::processMultiMC() instance.setName(m_instName); // if the icon was specified by user, use that. otherwise pull icon from the pack - if (m_instIcon != "default") - { + if (m_instIcon != "default") { instance.setIconKey(m_instIcon); - } - else - { + } else { m_instIcon = instance.iconKey(); auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon); - if (!importIconPath.isNull() && QFile::exists(importIconPath)) - { + if (!importIconPath.isNull() && QFile::exists(importIconPath)) { // import icon auto iconList = APPLICATION->icons(); - if (iconList->iconFileExists(m_instIcon)) - { + if (iconList->iconFileExists(m_instIcon)) { iconList->deleteIcon(m_instIcon); } - iconList->installIcons({importIconPath}); + iconList->installIcons({ importIconPath }); } } emitSucceeded(); } -void InstanceImportTask::processModrinth() { +void InstanceImportTask::processModrinth() +{ std::vector files; QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion; - try - { + try { QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); auto doc = Json::requireDocument(indexPath); auto obj = Json::requireObject(doc, "modrinth.index.json"); int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json"); - if (formatVersion == 1) - { + if (formatVersion == 1) { auto game = Json::requireString(obj, "game", "modrinth.index.json"); - if (game != "minecraft") - { + if (game != "minecraft") { throw JSONValidationError("Unknown game: " + game); } auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); - std::transform(jsonFiles.begin(), jsonFiles.end(), std::back_inserter(files), [](const QJsonObject& obj) - { - Modrinth::File file; - file.path = Json::requireString(obj, "path"); - QString supported = Json::ensureString(Json::ensureObject(obj, "env")); - QJsonObject hashes = Json::requireObject(obj, "hashes"); - QString hash; - QCryptographicHash::Algorithm hashAlgorithm; - hash = Json::ensureString(hashes, "sha1"); - hashAlgorithm = QCryptographicHash::Sha1; - if (hash.isEmpty()) - { - hash = Json::ensureString(hashes, "sha512"); - hashAlgorithm = QCryptographicHash::Sha512; - if (hash.isEmpty()) - { - hash = Json::ensureString(hashes, "sha256"); - hashAlgorithm = QCryptographicHash::Sha256; - if (hash.isEmpty()) - { - throw JSONValidationError("No hash found for: " + file.path); - } + bool had_optional = false; + for (auto& obj : jsonFiles) { + Modrinth::File file; + file.path = Json::requireString(obj, "path"); + + auto env = Json::ensureObject(obj, "env"); + QString support = Json::ensureString(env, "client", "unsupported"); + if (support == "unsupported") { + continue; + } else if (support == "optional") { + // TODO: Make a review dialog for choosing which ones the user wants! + if (!had_optional) { + had_optional = true; + auto info = CustomMessageBox::selectable( + m_parent, tr("Optional mod detected!"), + tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"), QMessageBox::Information); + info->exec(); + } + + if (file.path.endsWith(".jar")) + file.path += ".disabled"; + } + + QJsonObject hashes = Json::requireObject(obj, "hashes"); + QString hash; + QCryptographicHash::Algorithm hashAlgorithm; + hash = Json::ensureString(hashes, "sha1"); + hashAlgorithm = QCryptographicHash::Sha1; + if (hash.isEmpty()) { + hash = Json::ensureString(hashes, "sha512"); + hashAlgorithm = QCryptographicHash::Sha512; + if (hash.isEmpty()) { + hash = Json::ensureString(hashes, "sha256"); + hashAlgorithm = QCryptographicHash::Sha256; + if (hash.isEmpty()) { + throw JSONValidationError("No hash found for: " + file.path); } } - file.hash = QByteArray::fromHex(hash.toLatin1()); - file.hashAlgorithm = hashAlgorithm; - // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly handle spaces) - file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); - if (!file.download.isValid() || !Modrinth::validadeDownloadUrl(file.download)) - { - throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); - } - return file; - }); + } + file.hash = QByteArray::fromHex(hash.toLatin1()); + file.hashAlgorithm = hashAlgorithm; + // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly + // handle spaces) + file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); + if (!file.download.isValid() || !Modrinth::validadeDownloadUrl(file.download)) { + throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); + } + files.push_back(file); + } auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); - for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) - { + for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { QString name = it.key(); - if (name == "minecraft") - { + if (name == "minecraft") { if (!minecraftVersion.isEmpty()) throw JSONValidationError("Duplicate Minecraft version"); minecraftVersion = Json::requireString(*it, "Minecraft version"); - } - else if (name == "fabric-loader") - { + } else if (name == "fabric-loader") { if (!fabricVersion.isEmpty()) throw JSONValidationError("Duplicate Fabric Loader version"); fabricVersion = Json::requireString(*it, "Fabric Loader version"); - } - else if (name == "quilt-loader") - { + } else if (name == "quilt-loader") { if (!quiltVersion.isEmpty()) throw JSONValidationError("Duplicate Quilt Loader version"); quiltVersion = Json::requireString(*it, "Quilt Loader version"); - } - else if (name == "forge") - { + } else if (name == "forge") { if (!forgeVersion.isEmpty()) throw JSONValidationError("Duplicate Forge version"); forgeVersion = Json::requireString(*it, "Forge version"); - } - else - { + } else { throw JSONValidationError("Unknown dependency type: " + name); } } - } - else - { + } else { throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion)); } QFile::remove(indexPath); - } - catch (const JSONValidationError &e) - { + } catch (const JSONValidationError& e) { emitFailed(tr("Could not understand pack index:\n") + e.cause()); return; } diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 0dc6ba88..5e4d3235 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -55,7 +55,7 @@ class InstanceImportTask : public InstanceTask { Q_OBJECT public: - explicit InstanceImportTask(const QUrl sourceUrl); + explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr); bool canAbort() const override { return true; } bool abort() override; @@ -94,4 +94,7 @@ private: /* data */ Flame, Modrinth, } m_modpackType = ModpackType::Unknown; + + //FIXME: nuke + QWidget* m_parent; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index fadad9df..f24d3651 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -251,7 +251,7 @@ void ModrinthPage::suggestCurrent() for (auto& ver : current.versions) { if (ver.id == selectedVersion) { - dialog->setSuggestedPack(current.name + " " + ver.version, new InstanceImportTask(ver.download_url)); + dialog->setSuggestedPack(current.name + " " + ver.version, new InstanceImportTask(ver.download_url, this)); auto iconName = current.iconName; m_model->getLogo(iconName, current.iconUrl.toString(), [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); From 82760f4b916ef122eabb644e8679f9ae76587e44 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 16 May 2022 10:50:46 -0300 Subject: [PATCH 401/605] fix: import modrinth packs with weird overrides structure Probably because of Packwiz limitations, or an space optimizer that did this :) --- launcher/MMCZip.cpp | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index b92f1781..8591fcc0 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -297,20 +297,40 @@ nonstd::optional MMCZip::extractSubDir(QuaZip *zip, const QString & { continue; } + name.remove(0, subdir.size()); - QString absFilePath = directory.absoluteFilePath(name); + auto original_name = name; + + // Fix weird "folders with a single file get squashed" thing + QString path; + if(name.contains('/') && !name.endsWith('/')){ + path = name.section('/', 0, -2) + "/"; + FS::ensureFolderPathExists(path); + + name = name.split('/').last(); + } + + QString absFilePath; if(name.isEmpty()) { - absFilePath += "/"; + absFilePath = directory.absoluteFilePath(name) + "/"; } + else + { + absFilePath = directory.absoluteFilePath(path + name); + } + if (!JlCompress::extractFile(zip, "", absFilePath)) { - qWarning() << "Failed to extract file" << name << "to" << absFilePath; + qWarning() << "Failed to extract file" << original_name << "to" << absFilePath; JlCompress::removeFile(extracted); return nonstd::nullopt; } + extracted.append(absFilePath); - qDebug() << "Extracted file" << name; + QFile::setPermissions(absFilePath, QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser); + + qDebug() << "Extracted file" << name << "to" << absFilePath; } while (zip->goToNextFile()); return extracted; } From a6d2c5e18131ab155ed482aeab548dabc2741d62 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 16 May 2022 12:59:32 -0300 Subject: [PATCH 402/605] fix: better hack for icons that cant be natively scaled to 48x48 --- launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 0cf53659..bb54bc20 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -85,9 +85,10 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian return pack.description; } else if (role == Qt::DecorationRole) { if (m_logoMap.contains(pack.iconName)) { - return (m_logoMap.value(pack.iconName) - .pixmap(48, 48) - .scaled(48, 48, Qt::IgnoreAspectRatio, Qt::TransformationMode::SmoothTransformation)); + auto icon = m_logoMap.value(pack.iconName); + auto icon_scaled = QIcon(icon.pixmap(48, 48).scaledToWidth(48)); + + return icon_scaled; } QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); ((ModpackListModel*)this)->requestLogo(pack.iconName, pack.iconUrl.toString()); From cd9e0e0cc0228ffa24466814a649abef43045745 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 16 May 2022 20:17:19 +0200 Subject: [PATCH 403/605] fix: use own metacache base for modrinth icons --- launcher/Application.cpp | 1 + launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 11109857..afb33a50 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -819,6 +819,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath()); m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath()); m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath()); + m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath()); m_metacache->addBase("root", QDir::currentPath()); m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("icons", QDir("cache/icons").absolutePath()); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index bb54bc20..bc1046ad 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -208,7 +208,7 @@ void ModpackListModel::requestLogo(QString logo, QString url) } MetaEntryPtr entry = - APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); + APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0))); auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); From 2b52cf01f5999db4a8b1ea009cb5d24dd4eb4e1c Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sat, 14 May 2022 19:51:23 -0400 Subject: [PATCH 404/605] Build Windows installer --- .github/workflows/build.yml | 17 +++- program_info/win_install.nsi | 169 +++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 program_info/win_install.nsi diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0590b348..7ab30d45 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,6 +63,7 @@ jobs: ninja:p qt5:p ccache:p + nsis:p - name: Setup ccache if: runner.os != 'Windows' && inputs.build_type == 'Debug' @@ -100,7 +101,7 @@ jobs: run: | brew update brew install qt@5 ninja - + - name: Update Qt (AppImage) if: runner.os == 'Linux' && matrix.appimage == true run: | @@ -190,6 +191,13 @@ jobs: 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 + - name: Package (Windows, installer) + if: runner.os == 'Windows' + shell: msys2 {0} + run: | + cd ${{ env.INSTALL_PORTABLE_DIR }} + makensis -NOCD "-DVERSION=${{ env.VERSION }}" "-DMUI_ICON=${{ github.workspace }}/program_info/polymc.ico" "-XOutFile ${{ github.workspace }}/PolyMC-Setup.exe" "${{ github.workspace }}/program_info/win_install.nsi" + - name: Package (Linux) if: runner.os == 'Linux' && matrix.appimage != true run: | @@ -257,6 +265,13 @@ jobs: name: PolyMC-${{ matrix.name }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} path: ${{ env.INSTALL_PORTABLE_DIR }}/** + - name: Upload installer (Windows) + if: runner.os == 'Windows' + uses: actions/upload-artifact@v3 + with: + name: PolyMC-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}-Setup + path: PolyMC-Setup.exe + - name: Upload binary tarball (Linux) if: runner.os == 'Linux' && matrix.appimage != true uses: actions/upload-artifact@v3 diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi new file mode 100644 index 00000000..2b0d9760 --- /dev/null +++ b/program_info/win_install.nsi @@ -0,0 +1,169 @@ +!define MULTIUSER_EXECUTIONLEVEL Highest +!define MULTIUSER_MUI +!define MULTIUSER_INSTALLMODE_COMMANDLINE + +!define MULTIUSER_INSTALLMODE_INSTDIR PolyMC +!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_KEY Software\PolyMC +!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME InstallDir + +!include "FileFunc.nsh" +!include "MUI2.nsh" +!include "MultiUser.nsh" + +Name "PolyMC" +RequestExecutionLevel highest + +;-------------------------------- + +; Pages + +!insertmacro MUI_PAGE_WELCOME +!insertmacro MULTIUSER_PAGE_INSTALLMODE +!define MUI_COMPONENTSPAGE_NODESC +!insertmacro MUI_PAGE_COMPONENTS +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!define MUI_FINISHPAGE_RUN "$InstDir\polymc.exe" +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "English" + +;-------------------------------- + +; The stuff to install +Section "PolyMC" + + SectionIn RO + + nsExec::Exec /TIMEOUT=2000 'TaskKill /IM polymc.exe /F' + + SetOutPath $INSTDIR + + File "polymc.exe" + File "qt.conf" + File *.dll + File /r "iconengines" + File /r "imageformats" + File /r "jars" + File /r "platforms" + File /r "styles" + + ; Write the installation path into the registry + WriteRegStr SHCTX SOFTWARE\PolyMC "InstallDir" "$INSTDIR" + + ; Write the uninstall keys for Windows + !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" + WriteRegStr SHCTX "${UNINST_KEY}" "DisplayName" "PolyMC" + WriteRegStr SHCTX "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\polymc.exe" + WriteRegStr SHCTX "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe" /$MultiUser.InstallMode' + WriteRegStr SHCTX "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /$MultiUser.InstallMode /S' + WriteRegStr SHCTX "${UNINST_KEY}" "InstallLocation" "$INSTDIR" + WriteRegStr SHCTX "${UNINST_KEY}" "Publisher" "PolyMC Contributors" + WriteRegStr SHCTX "${UNINST_KEY}" "ProductVersion" "${VERSION}" + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD SHCTX "${UNINST_KEY}" "EstimatedSize" "$0" + WriteRegDWORD SHCTX "${UNINST_KEY}" "NoModify" 1 + WriteRegDWORD SHCTX "${UNINST_KEY}" "NoRepair" 1 + WriteUninstaller "$INSTDIR\uninstall.exe" + +SectionEnd + +Section "Start Menu Shortcuts" + + CreateShortcut "$SMPROGRAMS\PolyMC.lnk" "$INSTDIR\polymc.exe" "" "$INSTDIR\polymc.exe" 0 + +SectionEnd + +Section /o "Portable" + + SetOutPath $INSTDIR + File "portable.txt" + +SectionEnd + +;-------------------------------- + +; Uninstaller + +Section "Uninstall" + + nsExec::Exec /TIMEOUT=2000 'TaskKill /IM polymc.exe /F' + + DeleteRegKey SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" + DeleteRegKey SHCTX SOFTWARE\PolyMC + + Delete $INSTDIR\polymc.exe + Delete $INSTDIR\uninstall.exe + Delete $INSTDIR\portable.txt + + Delete $INSTDIR\libbrotlicommon.dll + Delete $INSTDIR\libbrotlidec.dll + Delete $INSTDIR\libbz2-1.dll + Delete $INSTDIR\libcrypto-1_1-x64.dll + Delete $INSTDIR\libcrypto-1_1.dll + Delete $INSTDIR\libdouble-conversion.dll + Delete $INSTDIR\libfreetype-6.dll + Delete $INSTDIR\libgcc_s_seh-1.dll + Delete $INSTDIR\libgcc_s_dw2-1.dll + Delete $INSTDIR\libglib-2.0-0.dll + Delete $INSTDIR\libgraphite2.dll + Delete $INSTDIR\libharfbuzz-0.dll + Delete $INSTDIR\libiconv-2.dll + Delete $INSTDIR\libicudt69.dll + Delete $INSTDIR\libicuin69.dll + Delete $INSTDIR\libicuuc69.dll + Delete $INSTDIR\libintl-8.dll + Delete $INSTDIR\libjasper-4.dll + Delete $INSTDIR\libjpeg-8.dll + Delete $INSTDIR\libmd4c.dll + Delete $INSTDIR\libpcre-1.dll + Delete $INSTDIR\libpcre2-16-0.dll + Delete $INSTDIR\libpng16-16.dll + Delete $INSTDIR\libssl-1_1-x64.dll + Delete $INSTDIR\libssl-1_1.dll + Delete $INSTDIR\libssp-0.dll + Delete $INSTDIR\libstdc++-6.dll + Delete $INSTDIR\libwebp-7.dll + Delete $INSTDIR\libwebpdemux-2.dll + Delete $INSTDIR\libwebpmux-3.dll + Delete $INSTDIR\libwinpthread-1.dll + Delete $INSTDIR\libzstd.dll + Delete $INSTDIR\Qt5Core.dll + Delete $INSTDIR\Qt5Gui.dll + Delete $INSTDIR\Qt5Network.dll + Delete $INSTDIR\Qt5Qml.dll + Delete $INSTDIR\Qt5QmlModels.dll + Delete $INSTDIR\Qt5Quick.dll + Delete $INSTDIR\Qt5Svg.dll + Delete $INSTDIR\Qt5WebSockets.dll + Delete $INSTDIR\Qt5Widgets.dll + Delete $INSTDIR\Qt5Xml.dll + Delete $INSTDIR\zlib1.dll + + Delete $INSTDIR\qt.conf + + RMDir /r $INSTDIR\iconengines + RMDir /r $INSTDIR\imageformats + RMDir /r $INSTDIR\jars + RMDir /r $INSTDIR\platforms + RMDir /r $INSTDIR\styles + + Delete "$SMPROGRAMS\PolyMC.lnk" + + RMDir "$INSTDIR" + +SectionEnd + +; Multi-user + +Function .onInit + !insertmacro MULTIUSER_INIT +FunctionEnd + +Function un.onInit + !insertmacro MULTIUSER_UNINIT +FunctionEnd From 2993318d195812f4b03c703aa9e68aeff941aece Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 16 May 2022 15:29:37 -0400 Subject: [PATCH 405/605] Remove admin requirement (no multi-user install option) --- program_info/win_install.nsi | 54 +++++++++++++----------------------- 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi index 2b0d9760..18a1b64e 100644 --- a/program_info/win_install.nsi +++ b/program_info/win_install.nsi @@ -1,24 +1,16 @@ -!define MULTIUSER_EXECUTIONLEVEL Highest -!define MULTIUSER_MUI -!define MULTIUSER_INSTALLMODE_COMMANDLINE - -!define MULTIUSER_INSTALLMODE_INSTDIR PolyMC -!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_KEY Software\PolyMC -!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME InstallDir - !include "FileFunc.nsh" !include "MUI2.nsh" -!include "MultiUser.nsh" Name "PolyMC" -RequestExecutionLevel highest +InstallDir "$LOCALAPPDATA\PolyMC" +InstallDirRegKey HKCU "Software\PolyMC" "InstallDir" +RequestExecutionLevel user ;-------------------------------- ; Pages !insertmacro MUI_PAGE_WELCOME -!insertmacro MULTIUSER_PAGE_INSTALLMODE !define MUI_COMPONENTSPAGE_NODESC !insertmacro MUI_PAGE_COMPONENTS !insertmacro MUI_PAGE_DIRECTORY @@ -29,6 +21,10 @@ RequestExecutionLevel highest !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES +;-------------------------------- + +; Languages + !insertmacro MUI_LANGUAGE "English" ;-------------------------------- @@ -52,22 +48,22 @@ Section "PolyMC" File /r "styles" ; Write the installation path into the registry - WriteRegStr SHCTX SOFTWARE\PolyMC "InstallDir" "$INSTDIR" + WriteRegStr HKCU Software\PolyMC "InstallDir" "$INSTDIR" ; Write the uninstall keys for Windows !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" - WriteRegStr SHCTX "${UNINST_KEY}" "DisplayName" "PolyMC" - WriteRegStr SHCTX "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\polymc.exe" - WriteRegStr SHCTX "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe" /$MultiUser.InstallMode' - WriteRegStr SHCTX "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /$MultiUser.InstallMode /S' - WriteRegStr SHCTX "${UNINST_KEY}" "InstallLocation" "$INSTDIR" - WriteRegStr SHCTX "${UNINST_KEY}" "Publisher" "PolyMC Contributors" - WriteRegStr SHCTX "${UNINST_KEY}" "ProductVersion" "${VERSION}" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "PolyMC" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\polymc.exe" + WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe"' + WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S' + WriteRegStr HKCU "${UNINST_KEY}" "InstallLocation" "$INSTDIR" + WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "PolyMC Contributors" + WriteRegStr HKCU "${UNINST_KEY}" "ProductVersion" "${VERSION}" ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 IntFmt $0 "0x%08X" $0 - WriteRegDWORD SHCTX "${UNINST_KEY}" "EstimatedSize" "$0" - WriteRegDWORD SHCTX "${UNINST_KEY}" "NoModify" 1 - WriteRegDWORD SHCTX "${UNINST_KEY}" "NoRepair" 1 + WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0" + WriteRegDWORD HKCU "${UNINST_KEY}" "NoModify" 1 + WriteRegDWORD HKCU "${UNINST_KEY}" "NoRepair" 1 WriteUninstaller "$INSTDIR\uninstall.exe" SectionEnd @@ -93,8 +89,8 @@ Section "Uninstall" nsExec::Exec /TIMEOUT=2000 'TaskKill /IM polymc.exe /F' - DeleteRegKey SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" - DeleteRegKey SHCTX SOFTWARE\PolyMC + DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" + DeleteRegKey HKCU SOFTWARE\PolyMC Delete $INSTDIR\polymc.exe Delete $INSTDIR\uninstall.exe @@ -157,13 +153,3 @@ Section "Uninstall" RMDir "$INSTDIR" SectionEnd - -; Multi-user - -Function .onInit - !insertmacro MULTIUSER_INIT -FunctionEnd - -Function un.onInit - !insertmacro MULTIUSER_UNINIT -FunctionEnd From 887246a66b5391b16d5b0d275ba77fe3a8bf540b Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 16 May 2022 17:05:54 -0300 Subject: [PATCH 406/605] fix: typo and useless code --- launcher/InstanceImportTask.cpp | 27 +++++++++---------- .../modrinth/ModrinthPackManifest.cpp | 4 +-- .../modrinth/ModrinthPackManifest.h | 2 +- .../modplatform/modrinth/ModrinthPage.cpp | 6 ----- .../pages/modplatform/modrinth/ModrinthPage.h | 1 - 5 files changed, 15 insertions(+), 25 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 64f2dd02..8f68b95f 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -554,10 +554,10 @@ void InstanceImportTask::processModrinth() } file.hash = QByteArray::fromHex(hash.toLatin1()); file.hashAlgorithm = hashAlgorithm; - // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly - // handle spaces) + // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode + // (as Modrinth seems to incorrectly handle spaces) file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); - if (!file.download.isValid() || !Modrinth::validadeDownloadUrl(file.download)) { + if (!file.download.isValid() || !Modrinth::validateDownloadUrl(file.download)) { throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); } files.push_back(file); @@ -567,22 +567,18 @@ void InstanceImportTask::processModrinth() for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { QString name = it.key(); if (name == "minecraft") { - if (!minecraftVersion.isEmpty()) - throw JSONValidationError("Duplicate Minecraft version"); minecraftVersion = Json::requireString(*it, "Minecraft version"); - } else if (name == "fabric-loader") { - if (!fabricVersion.isEmpty()) - throw JSONValidationError("Duplicate Fabric Loader version"); + } + else if (name == "fabric-loader") { fabricVersion = Json::requireString(*it, "Fabric Loader version"); - } else if (name == "quilt-loader") { - if (!quiltVersion.isEmpty()) - throw JSONValidationError("Duplicate Quilt Loader version"); + } + else if (name == "quilt-loader") { quiltVersion = Json::requireString(*it, "Quilt Loader version"); - } else if (name == "forge") { - if (!forgeVersion.isEmpty()) - throw JSONValidationError("Duplicate Forge version"); + } + else if (name == "forge") { forgeVersion = Json::requireString(*it, "Forge version"); - } else { + } + else { throw JSONValidationError("Unknown dependency type: " + name); } } @@ -594,6 +590,7 @@ void InstanceImportTask::processModrinth() emitFailed(tr("Could not understand pack index:\n") + e.cause()); return; } + QString overridePath = FS::PathCombine(m_stagingPath, "overrides"); if (QFile::exists(overridePath)) { QString mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 947ac182..f1ad39ce 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -93,7 +93,7 @@ void loadIndexedVersions(Modpack& pack, QJsonDocument& doc) pack.versionsLoaded = true; } -auto validadeDownloadUrl(QUrl url) -> bool +auto validateDownloadUrl(QUrl url) -> bool { auto domain = url.host(); if(domain == "cdn.modrinth.com") @@ -139,7 +139,7 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion auto url = Json::requireString(parent, "url"); - if(!validadeDownloadUrl(url)) + if(!validateDownloadUrl(url)) continue; file.download_url = url; diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 4db4a75d..e5fc9a70 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -99,7 +99,7 @@ void loadIndexedInfo(Modpack&, QJsonObject&); void loadIndexedVersions(Modpack&, QJsonDocument&); auto loadIndexedVersion(QJsonObject&) -> ModpackVersion; -auto validadeDownloadUrl(QUrl) -> bool; +auto validateDownloadUrl(QUrl) -> bool; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index f24d3651..9bd24b57 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -195,7 +195,6 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) ui->versionSelectionBox->addItem(version.version, QVariant(version.id)); } - updateVersionsUI(); suggestCurrent(); }); QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { @@ -233,11 +232,6 @@ void ModrinthPage::updateUI() ui->packDescription->setHtml(text + current.description); } -void ModrinthPage::updateVersionsUI() -{ - // idk -} - void ModrinthPage::suggestCurrent() { if (!isOpened) { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 9aa702f9..db5e1a3d 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -71,7 +71,6 @@ class ModrinthPage : public QWidget, public BasePage { void suggestCurrent(); void updateUI(); - void updateVersionsUI(); void retranslate() override; void openedImpl() override; From 696a711e397440275a55f4dbe02947a78ab0b208 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 16 May 2022 19:10:31 -0300 Subject: [PATCH 407/605] fix: missed change to metacache entry lookup --- launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index bc1046ad..701a2032 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -194,7 +194,7 @@ void ModpackListModel::getLogo(const QString& logo, const QString& logoUrl, Logo { if (m_logoMap.contains(logo)) { callback(APPLICATION->metacache() - ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))) + ->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0))) ->getFullPath()); } else { requestLogo(logo, logoUrl); From 2e9d7f5c3d3cbc33ad95d830af4fdcab6eab6a06 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 16 May 2022 19:17:37 -0300 Subject: [PATCH 408/605] fix: mod skipping between pages and remove dead code --- .../modplatform/modrinth/ModrinthModel.cpp | 30 ++++--------------- .../modplatform/modrinth/ModrinthModel.h | 4 +-- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 701a2032..7cacf37a 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -109,11 +109,12 @@ void ModpackListModel::performPaginatedSearch() auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL + "/search?" "offset=%1&" - "limit=20&" - "query=%2&" - "index=%3&" + "limit=%2&" + "query=%3&" + "index=%4&" "facets=[[\"project_type:modpack\"]]") .arg(nextSearchOffset) + .arg(m_modpacks_per_page) .arg(currentSearchTerm) .arg(currentSort); @@ -269,10 +270,10 @@ void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all) } } - if (packs_all.size() < 25) { + if (packs_all.size() < m_modpacks_per_page) { searchState = Finished; } else { - nextSearchOffset += 25; + nextSearchOffset += m_modpacks_per_page; searchState = CanPossiblyFetchMore; } @@ -308,25 +309,6 @@ void ModpackListModel::searchRequestFailed(QString reason) } } -void ModpackListModel::versionRequestSucceeded(QJsonDocument doc, QString id) -{ - auto& current = m_parent->getCurrent(); - if (id != current.id) { - return; - } - - auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); - - try { - // loadIndexedPackVersions(current, arr); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause(); - } - - // m_parent->updateModVersions(); -} - } // namespace Modrinth /******** Helpers ********/ diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index bffea54d..14aa6747 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -79,8 +79,6 @@ class ModpackListModel : public QAbstractListModel { void searchRequestFinished(QJsonDocument& doc_all); void searchRequestFailed(QString reason); - void versionRequestSucceeded(QJsonDocument doc, QString addonId); - protected slots: void logoFailed(QString logo); @@ -112,5 +110,7 @@ class ModpackListModel : public QAbstractListModel { QByteArray m_all_response; QByteArray m_specific_response; + + int m_modpacks_per_page = 20; }; } // namespace ModPlatform From 6dfec4db40f09697f34f65419edb7d689e3c5dc7 Mon Sep 17 00:00:00 2001 From: Lenny McLennington Date: Tue, 17 May 2022 00:21:57 +0100 Subject: [PATCH 409/605] Fix toolbar disappearing in a certain circumstance. --- launcher/ui/MainWindow.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index ca345b1f..3f854511 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1865,6 +1865,9 @@ void MainWindow::globalSettingsClosed() updateMainToolBar(); updateToolsMenu(); updateStatusCenter(); + // This needs to be done to prevent UI elements disappearing in the event the config is changed + // but PolyMC exits abnormally, causing the window state to never be saved: + APPLICATION->settings()->set("MainWindowState", saveState().toBase64()); update(); } From 85ec9d95a43cc884224095477e7321b84d2cc99f Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 16 May 2022 19:28:04 -0400 Subject: [PATCH 410/605] Support installer languages other than English --- program_info/win_install.nsi | 68 ++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi index 18a1b64e..ce13b8b0 100644 --- a/program_info/win_install.nsi +++ b/program_info/win_install.nsi @@ -1,6 +1,8 @@ !include "FileFunc.nsh" !include "MUI2.nsh" +Unicode true + Name "PolyMC" InstallDir "$LOCALAPPDATA\PolyMC" InstallDirRegKey HKCU "Software\PolyMC" "InstallDir" @@ -26,6 +28,72 @@ RequestExecutionLevel user ; Languages !insertmacro MUI_LANGUAGE "English" +!insertmacro MUI_LANGUAGE "French" +!insertmacro MUI_LANGUAGE "German" +!insertmacro MUI_LANGUAGE "Spanish" +!insertmacro MUI_LANGUAGE "SpanishInternational" +!insertmacro MUI_LANGUAGE "SimpChinese" +!insertmacro MUI_LANGUAGE "TradChinese" +!insertmacro MUI_LANGUAGE "Japanese" +!insertmacro MUI_LANGUAGE "Korean" +!insertmacro MUI_LANGUAGE "Italian" +!insertmacro MUI_LANGUAGE "Dutch" +!insertmacro MUI_LANGUAGE "Danish" +!insertmacro MUI_LANGUAGE "Swedish" +!insertmacro MUI_LANGUAGE "Norwegian" +!insertmacro MUI_LANGUAGE "NorwegianNynorsk" +!insertmacro MUI_LANGUAGE "Finnish" +!insertmacro MUI_LANGUAGE "Greek" +!insertmacro MUI_LANGUAGE "Russian" +!insertmacro MUI_LANGUAGE "Portuguese" +!insertmacro MUI_LANGUAGE "PortugueseBR" +!insertmacro MUI_LANGUAGE "Polish" +!insertmacro MUI_LANGUAGE "Ukrainian" +!insertmacro MUI_LANGUAGE "Czech" +!insertmacro MUI_LANGUAGE "Slovak" +!insertmacro MUI_LANGUAGE "Croatian" +!insertmacro MUI_LANGUAGE "Bulgarian" +!insertmacro MUI_LANGUAGE "Hungarian" +!insertmacro MUI_LANGUAGE "Thai" +!insertmacro MUI_LANGUAGE "Romanian" +!insertmacro MUI_LANGUAGE "Latvian" +!insertmacro MUI_LANGUAGE "Macedonian" +!insertmacro MUI_LANGUAGE "Estonian" +!insertmacro MUI_LANGUAGE "Turkish" +!insertmacro MUI_LANGUAGE "Lithuanian" +!insertmacro MUI_LANGUAGE "Slovenian" +!insertmacro MUI_LANGUAGE "Serbian" +!insertmacro MUI_LANGUAGE "SerbianLatin" +!insertmacro MUI_LANGUAGE "Arabic" +!insertmacro MUI_LANGUAGE "Farsi" +!insertmacro MUI_LANGUAGE "Hebrew" +!insertmacro MUI_LANGUAGE "Indonesian" +!insertmacro MUI_LANGUAGE "Mongolian" +!insertmacro MUI_LANGUAGE "Luxembourgish" +!insertmacro MUI_LANGUAGE "Albanian" +!insertmacro MUI_LANGUAGE "Breton" +!insertmacro MUI_LANGUAGE "Belarusian" +!insertmacro MUI_LANGUAGE "Icelandic" +!insertmacro MUI_LANGUAGE "Malay" +!insertmacro MUI_LANGUAGE "Bosnian" +!insertmacro MUI_LANGUAGE "Kurdish" +!insertmacro MUI_LANGUAGE "Irish" +!insertmacro MUI_LANGUAGE "Uzbek" +!insertmacro MUI_LANGUAGE "Galician" +!insertmacro MUI_LANGUAGE "Afrikaans" +!insertmacro MUI_LANGUAGE "Catalan" +!insertmacro MUI_LANGUAGE "Esperanto" +!insertmacro MUI_LANGUAGE "Asturian" +!insertmacro MUI_LANGUAGE "Basque" +!insertmacro MUI_LANGUAGE "Pashto" +!insertmacro MUI_LANGUAGE "ScotsGaelic" +!insertmacro MUI_LANGUAGE "Georgian" +!insertmacro MUI_LANGUAGE "Vietnamese" +!insertmacro MUI_LANGUAGE "Welsh" +!insertmacro MUI_LANGUAGE "Armenian" +!insertmacro MUI_LANGUAGE "Corsican" +!insertmacro MUI_LANGUAGE "Tatar" +!insertmacro MUI_LANGUAGE "Hindi" ;-------------------------------- From 96deb5b09d6729f4b5f3a5c1880b526ee9882326 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 17 May 2022 06:36:30 -0300 Subject: [PATCH 411/605] chore: remove copyright from files i didnt mess with This is what happens when you auto-pilot stuff xdd --- launcher/net/PasteUpload.cpp | 1 - launcher/net/PasteUpload.h | 1 - launcher/net/Validator.h | 1 - 3 files changed, 3 deletions(-) diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index e88c8987..3d106c92 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - 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 diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index 53979352..ea3a06d3 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - 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 diff --git a/launcher/net/Validator.h b/launcher/net/Validator.h index e1d71d1c..6b3d4635 100644 --- a/launcher/net/Validator.h +++ b/launcher/net/Validator.h @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - 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 From 17bbfe8d8951ddc7acca0222c6d2e38fb29eef25 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 17 May 2022 06:47:00 -0300 Subject: [PATCH 412/605] fix: virtual signal in Task.h --- launcher/tasks/Task.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index f0e6e402..f7765c3d 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -86,7 +86,7 @@ class Task : public QObject { signals: void started(); - virtual void progress(qint64 current, qint64 total); + void progress(qint64 current, qint64 total); void finished(); void succeeded(); void aborted(); From ddc3b5eb0bbd17edd60d96f5094d65dbdb922765 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 17 May 2022 15:14:53 +0200 Subject: [PATCH 413/605] Update launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui Co-authored-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> --- launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 90e8dba3..4fb59cdf 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -19,7 +19,7 @@ - Note: Modrinth modpacks is still in alpha phase. Some things may be rough on the edges, or not working at all! Use it with caution. + Note: Modrinth modpacks are still in alpha phase. Some things may be rough on the edges, or not working at all! Use it with caution. Qt::AlignCenter From edbd90a4e671486bdc1ca6f8f051445ae473fdcc Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 17 May 2022 15:17:20 +0200 Subject: [PATCH 414/605] fix: update links for Quilt metadata format --- launcher/minecraft/mod/LocalModParseTask.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/LocalModParseTask.cpp index 699cf7ee..631c3abb 100644 --- a/launcher/minecraft/mod/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/LocalModParseTask.cpp @@ -263,7 +263,7 @@ std::shared_ptr ReadFabricModInfo(QByteArray contents) return details; } -// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md#the-schema_version-field +// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md std::shared_ptr ReadQuiltModInfo(QByteArray contents) { QJsonParseError jsonError; @@ -273,6 +273,7 @@ std::shared_ptr ReadQuiltModInfo(QByteArray contents) std::shared_ptr details = std::make_shared(); + // https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md if (schemaVersion == 1) { auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info"); From c1700054f4d3e7a070fd0b9b25f6ccb67b72aa06 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 4 Feb 2022 15:02:12 +0100 Subject: [PATCH 415/605] fix: replace deprecated stuff as of Qt 5.12 --- launcher/ui/dialogs/ExportInstanceDialog.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 5fac1015..8631edf6 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -117,7 +117,7 @@ public: flags |= Qt::ItemIsUserCheckable; if (sourceIndex.model()->hasChildren(sourceIndex)) { - flags |= Qt::ItemIsTristate; + flags |= Qt::ItemIsAutoTristate; } } @@ -210,7 +210,7 @@ public: QStack todo; while (1) { - auto node = doing.child(row, 0); + auto node = fsm->index(row, 0, doing); if (!node.isValid()) { if (!todo.size()) @@ -259,7 +259,7 @@ public: QStack todo; while (1) { - auto node = doing.child(row, 0); + auto node = this->index(row, 0, doing); if (!node.isValid()) { if (!todo.size()) @@ -460,7 +460,7 @@ void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom) //WARNING: possible off-by-one? for(int i = top; i < bottom; i++) { - auto node = parent.child(i, 0); + auto node = proxyModel->index(i, 0, parent); if(proxyModel->shouldExpand(node)) { auto expNode = node.parent(); From cc27bb3231e355b147c529695317cf15e84178e1 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 4 Feb 2022 15:31:49 +0100 Subject: [PATCH 416/605] fix(updater): remove Windows version check Qt 5.12 doesn't support anything older than Windows 7 anyway, so we can't really check if we are on an older platform. --- launcher/UpdateController.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/launcher/UpdateController.cpp b/launcher/UpdateController.cpp index c02cd1e7..646f8e57 100644 --- a/launcher/UpdateController.cpp +++ b/launcher/UpdateController.cpp @@ -138,20 +138,6 @@ void UpdateController::installUpdates() } #endif QFileInfo destination (FS::PathCombine(m_root, op.destination)); -#ifdef Q_OS_WIN32 - if(QSysInfo::windowsVersion() < QSysInfo::WV_VISTA) - { - if(destination.fileName() == windowsExeName) - { - QDir rootDir(m_root); - exeOrigin = rootDir.relativeFilePath(op.source); - exePath = rootDir.relativeFilePath(op.destination); - exeBackup = rootDir.relativeFilePath(FS::PathCombine(backupPath, destination.fileName())); - useXPHack = true; - continue; - } - } -#endif if(destination.exists()) { QString backupName = op.destination; From 4b06fc53235ab7c16ec819dd7e9d1b89aad19db9 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 3 Feb 2022 23:11:10 +0100 Subject: [PATCH 417/605] chore!: drop support for Qt <5.12 BREAKING CHANGE: If there are references to stuff that's deprecated as of Qt 5.12, the compilation will fail. This means that support for versions below 5.12 is hereby dropped --- CMakeLists.txt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d3683d7..dde3578f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) # minimum version required by QuaZip if(WIN32) # In Qt 5.1+ we have our own main() function, don't autolink to qtmain on Windows @@ -34,14 +34,12 @@ set(CMAKE_C_STANDARD_REQUIRED true) set(CMAKE_CXX_STANDARD 11) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) -set(CMAKE_CXX_FLAGS " -Wall -pedantic -Werror -Wno-deprecated-declarations -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS " -Wall -pedantic -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") if(UNIX AND APPLE) set(CMAKE_CXX_FLAGS " -stdlib=libc++ ${CMAKE_CXX_FLAGS}") endif() set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror=return-type") - -# 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") option(ENABLE_LTO "Enable Link Time Optimization" off) From 8e9f1bcf18f5914ff448c640d492eaf24f436302 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 13 Feb 2022 20:04:49 +0100 Subject: [PATCH 418/605] fix: remove unnecessary Qt version checks --- launcher/Application.cpp | 4 +--- launcher/main.cpp | 2 -- launcher/ui/pages/global/ExternalToolsPage.cpp | 2 -- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index afb33a50..dc8a7b0d 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -223,9 +223,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) setApplicationName(BuildConfig.LAUNCHER_NAME); setApplicationDisplayName(BuildConfig.LAUNCHER_DISPLAYNAME); setApplicationVersion(BuildConfig.printableVersionString()); - #if (QT_VERSION >= QT_VERSION_CHECK(5,7,0)) - setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME); - #endif + setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME); startTime = QDateTime::currentDateTime(); // Don't quit on hiding the last window diff --git a/launcher/main.cpp b/launcher/main.cpp index 275fff32..85c5fdee 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -24,10 +24,8 @@ int main(int argc, char *argv[]) return 42; #endif -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); -#endif #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); diff --git a/launcher/ui/pages/global/ExternalToolsPage.cpp b/launcher/ui/pages/global/ExternalToolsPage.cpp index 693ca5c1..5ba0ebc2 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.cpp +++ b/launcher/ui/pages/global/ExternalToolsPage.cpp @@ -54,9 +54,7 @@ ExternalToolsPage::ExternalToolsPage(QWidget *parent) : ui->setupUi(this); ui->tabWidget->tabBar()->hide(); - #if QT_VERSION >= QT_VERSION_CHECK(5, 2, 0) ui->jsonEditorTextBox->setClearButtonEnabled(true); - #endif ui->mceditLink->setOpenExternalLinks(true); ui->jvisualvmLink->setOpenExternalLinks(true); From a21bd41580ba8ba4c0052efa8196d617e7211335 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 16 May 2022 22:37:26 +0200 Subject: [PATCH 419/605] fix: ignore deprecation again --- CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dde3578f..e07d2aa6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,11 +34,15 @@ set(CMAKE_C_STANDARD_REQUIRED true) set(CMAKE_CXX_STANDARD 11) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) -set(CMAKE_CXX_FLAGS " -Wall -pedantic -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS " -Wall -pedantic -Werror -Wno-deprecated-declarations -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") if(UNIX AND APPLE) set(CMAKE_CXX_FLAGS " -stdlib=libc++ ${CMAKE_CXX_FLAGS}") endif() set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror=return-type") + +# 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") option(ENABLE_LTO "Enable Link Time Optimization" off) From 127dfadc6cd2f0c72816af86716e88c3b4af2848 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Wed, 18 May 2022 14:33:58 +0200 Subject: [PATCH 420/605] fix(quilt) always prefer qmj over fmj this fixes Quilt-only mods like ok zoomer showing wrong metadata --- launcher/minecraft/mod/LocalModParseTask.cpp | 26 ++++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/launcher/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/LocalModParseTask.cpp index 631c3abb..a7bec5ae 100644 --- a/launcher/minecraft/mod/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/LocalModParseTask.cpp @@ -430,19 +430,6 @@ void LocalModParseTask::processAsZip() zip.close(); return; } - else if (zip.setCurrentFile("fabric.mod.json")) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - - m_result->details = ReadFabricModInfo(file.readAll()); - file.close(); - zip.close(); - return; - } else if (zip.setCurrentFile("quilt.mod.json")) { if (!file.open(QIODevice::ReadOnly)) @@ -456,6 +443,19 @@ void LocalModParseTask::processAsZip() zip.close(); return; } + else if (zip.setCurrentFile("fabric.mod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadFabricModInfo(file.readAll()); + file.close(); + zip.close(); + return; + } else if (zip.setCurrentFile("forgeversion.properties")) { if (!file.open(QIODevice::ReadOnly)) From 441075f61051cce8e5d6b0311febdefc087fdbbf Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 18 May 2022 17:17:16 -0300 Subject: [PATCH 421/605] fix: version field in technic pack manifest being null Sometimes, the version field, that is supposed to be a string, was a null instead. Inspecting other entries, seems like the default for not having a version should be "", so I made it like that in case the version was null. I hope this fixes the issue :^) --- launcher/modplatform/technic/SolderPackManifest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/technic/SolderPackManifest.cpp b/launcher/modplatform/technic/SolderPackManifest.cpp index 16fe0b0e..e52a7ec0 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::requireString(obj, "version"); + b.version = Json::ensureString(obj, "version", ""); b.md5 = Json::requireString(obj, "md5"); b.url = Json::requireString(obj, "url"); } From f66e0fa0e8cdd39ca9561f72759377db468f94b7 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 18 May 2022 22:51:14 +0200 Subject: [PATCH 422/605] fix: support split natives Mojang introduced a new structure for natives, notably for LWJGL. Now instead of using the `natives` structure of the version format, Mojang chose to create a seperate library entry for each platform, which uses the `rules` structure to specify the platform. These new split natives carry the same groupId and artifactId, as the main library, but have an additional classifier, like `natives-linux`. When comparing GradleSpecifiers we don't look at the classifier, so when the launcher sees an artifact called `org.lwjgl:lwjgl:3.3.1` and right after that an artifact called `org.lwjgl:lwjgl:3.3.1:natives-linux`, it will treat it as "already added" and forget it. This change will include the classifier in that comparison. --- launcher/minecraft/GradleSpecifier.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/GradleSpecifier.h b/launcher/minecraft/GradleSpecifier.h index 60e0a726..d9bb0207 100644 --- a/launcher/minecraft/GradleSpecifier.h +++ b/launcher/minecraft/GradleSpecifier.h @@ -124,7 +124,7 @@ struct GradleSpecifier } bool matchName(const GradleSpecifier & other) const { - return other.artifactId() == artifactId() && other.groupId() == groupId(); + return other.artifactId() == artifactId() && other.groupId() == groupId() && other.classifier() == classifier(); } bool operator==(const GradleSpecifier & other) const { From 77caaca50dab7ba8e455d641ac6b448052bc6799 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 19 May 2022 08:09:18 +0200 Subject: [PATCH 423/605] fix: only consider enabled mod loaders --- launcher/minecraft/PackProfile.cpp | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index d53f41e1..87d11c4c 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -36,6 +36,13 @@ #include "ComponentUpdateTask.h" #include "Application.h" +#include "modplatform/ModAPI.h" + +static const QMap modloaderMapping{ + {"net.minecraftforge", ModAPI::Forge}, + {"net.fabricmc.fabric-loader", ModAPI::Fabric}, + {"org.quiltmc.quilt-loader", ModAPI::Quilt} +}; PackProfile::PackProfile(MinecraftInstance * instance) : QAbstractListModel() @@ -973,17 +980,15 @@ void PackProfile::disableInteraction(bool disable) ModAPI::ModLoaderType PackProfile::getModLoader() { - if (!getComponentVersion("net.minecraftforge").isEmpty()) + QMapIterator i(modloaderMapping); + + while (i.hasNext()) { - return ModAPI::Forge; - } - else if (!getComponentVersion("net.fabricmc.fabric-loader").isEmpty()) - { - return ModAPI::Fabric; - } - else if (!getComponentVersion("org.quiltmc.quilt-loader").isEmpty()) - { - return ModAPI::Quilt; + i.next(); + Component* c = getComponent(i.key()); + if (c != nullptr && c->isEnabled()) { + return i.value(); + } } return ModAPI::Unspecified; } From 943090db98dbbe969afed8a4fb59f4bbb43449cc Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 19 May 2022 08:40:28 +0200 Subject: [PATCH 424/605] refactor: allow tracking multiple mod loaders --- launcher/minecraft/PackProfile.cpp | 8 +++-- launcher/minecraft/PackProfile.h | 2 +- launcher/modplatform/ModAPI.h | 15 +++++--- launcher/modplatform/flame/FlameAPI.h | 17 +++++---- launcher/modplatform/modrinth/ModrinthAPI.h | 35 ++++++++----------- launcher/ui/pages/instance/ModFolderPage.cpp | 2 +- launcher/ui/pages/modplatform/ModModel.cpp | 4 +-- launcher/ui/pages/modplatform/ModPage.cpp | 2 +- launcher/ui/pages/modplatform/ModPage.h | 2 +- .../pages/modplatform/flame/FlameModPage.cpp | 4 +-- .../ui/pages/modplatform/flame/FlameModPage.h | 2 +- .../modplatform/modrinth/ModrinthModPage.cpp | 4 +-- .../modplatform/modrinth/ModrinthModPage.h | 2 +- 13 files changed, 54 insertions(+), 45 deletions(-) diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 87d11c4c..125048f0 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -978,8 +978,10 @@ void PackProfile::disableInteraction(bool disable) } } -ModAPI::ModLoaderType PackProfile::getModLoader() +ModAPI::ModLoaderTypes PackProfile::getModLoaders() { + ModAPI::ModLoaderTypes result = ModAPI::Unspecified; + QMapIterator i(modloaderMapping); while (i.hasNext()) @@ -987,8 +989,8 @@ ModAPI::ModLoaderType PackProfile::getModLoader() i.next(); Component* c = getComponent(i.key()); if (c != nullptr && c->isEnabled()) { - return i.value(); + result |= i.value(); } } - return ModAPI::Unspecified; + return result; } diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index ab4cd5c8..918e7f7a 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -118,7 +118,7 @@ public: // todo(merged): is this the best approach void appendComponent(ComponentPtr component); - ModAPI::ModLoaderType getModLoader(); + ModAPI::ModLoaderTypes getModLoaders(); private: void scheduleSave(); diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index 8e6cd45c..4230df0b 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -16,14 +16,21 @@ class ModAPI { public: virtual ~ModAPI() = default; - // https://docs.curseforge.com/?http#tocS_ModLoaderType - enum ModLoaderType { Unspecified = 0, Forge = 1, Cauldron = 2, LiteLoader = 3, Fabric = 4, Quilt = 5 }; + enum ModLoaderType { + Unspecified = 0, + Forge = 1 << 0, + Cauldron = 1 << 1, + LiteLoader = 1 << 2, + Fabric = 1 << 3, + Quilt = 1 << 4 + }; + Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) struct SearchArgs { int offset; QString search; QString sorting; - ModLoaderType mod_loader; + ModLoaderTypes loaders; std::list versions; }; @@ -33,7 +40,7 @@ class ModAPI { struct VersionSearchArgs { QString addonId; std::list mcVersions; - ModLoaderType loader; + ModLoaderTypes loaders; }; virtual void getVersions(CallerType* caller, VersionSearchArgs&& args) const = 0; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 61628e60..8bb33d47 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -37,14 +37,14 @@ class FlameAPI : public NetworkModAPI { .arg(args.offset) .arg(args.search) .arg(getSortFieldInt(args.sorting)) - .arg(getMappedModLoader(args.mod_loader)) + .arg(getMappedModLoader(args.loaders)) .arg(gameVersionStr); }; inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { QString gameVersionQuery = args.mcVersions.size() == 1 ? QString("gameVersion=%1&").arg(args.mcVersions.front().toString()) : ""; - QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loader)); + QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loaders)); return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&%2%3") .arg(args.addonId) @@ -53,11 +53,16 @@ class FlameAPI : public NetworkModAPI { }; public: - static auto getMappedModLoader(const ModLoaderType type) -> const ModLoaderType + static auto getMappedModLoader(const ModLoaderTypes loaders) -> const int { + // https://docs.curseforge.com/?http#tocS_ModLoaderType + if (loaders & Forge) + return 1; + if (loaders & Fabric) + return 4; // TODO: remove this once Quilt drops official Fabric support - if (type == Quilt) // NOTE: Most if not all Fabric mods should work *currently* - return Fabric; - return type; + if (loaders & Quilt) // NOTE: Most if not all Fabric mods should work *currently* + return 4; // Quilt would probably be 5 + return 0; } }; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 6d642b5e..39f6c49a 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -28,30 +28,25 @@ class ModrinthAPI : public NetworkModAPI { public: inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; - static auto getModLoaderStrings(ModLoaderType type) -> const QStringList + static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList { QStringList l; - switch (type) + for (auto loader : {Forge, Fabric, Quilt}) { - case Unspecified: - for (auto loader : {Forge, Fabric, Quilt}) - { - l << ModAPI::getModLoaderString(loader); - } - break; - - case Quilt: - l << ModAPI::getModLoaderString(Fabric); - default: - l << ModAPI::getModLoaderString(type); + if (types & loader || types == Unspecified) + { + l << ModAPI::getModLoaderString(loader); + } } + if (types & Quilt && ~types & Fabric) // Add Fabric if Quilt is in use, if Fabric isn't already there + l << ModAPI::getModLoaderString(Fabric); return l; } - static auto getModLoaderFilters(ModLoaderType type) -> const QString + static auto getModLoaderFilters(ModLoaderTypes types) -> const QString { QStringList l; - for (auto loader : getModLoaderStrings(type)) + for (auto loader : getModLoaderStrings(types)) { l << QString("\"categories:%1\"").arg(loader); } @@ -61,7 +56,7 @@ class ModrinthAPI : public NetworkModAPI { private: inline auto getModSearchURL(SearchArgs& args) const -> QString override { - if (!validateModLoader(args.mod_loader)) { + if (!validateModLoaders(args.loaders)) { qWarning() << "Modrinth only have Forge and Fabric-compatible mods!"; return ""; } @@ -76,7 +71,7 @@ class ModrinthAPI : public NetworkModAPI { .arg(args.offset) .arg(args.search) .arg(args.sorting) - .arg(getModLoaderFilters(args.mod_loader)) + .arg(getModLoaderFilters(args.loaders)) .arg(getGameVersionsArray(args.versions)); }; @@ -88,7 +83,7 @@ class ModrinthAPI : public NetworkModAPI { "loaders=[\"%3\"]") .arg(args.addonId) .arg(getGameVersionsString(args.mcVersions)) - .arg(getModLoaderStrings(args.loader).join("\",\"")); + .arg(getModLoaderStrings(args.loaders).join("\",\"")); }; auto getGameVersionsArray(std::list mcVersions) const -> QString @@ -101,9 +96,9 @@ class ModrinthAPI : public NetworkModAPI { return s.isEmpty() ? QString() : QString("[%1],").arg(s); } - inline auto validateModLoader(ModLoaderType modLoader) const -> bool + inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool { - return modLoader == Unspecified || modLoader == Forge || modLoader == Fabric || modLoader == Quilt; + return loaders == Unspecified || loaders & (Forge | Fabric | Quilt); } }; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 8113fe85..5574f9d2 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -391,7 +391,7 @@ void ModFolderPage::on_actionInstall_mods_triggered() return; //this is a null instance or a legacy instance } auto profile = ((MinecraftInstance *)m_inst)->getPackProfile(); - if (profile->getModLoader() == ModAPI::Unspecified) { + if (profile->getModLoaders() == ModAPI::Unspecified) { QMessageBox::critical(this,tr("Error"),tr("Please install a mod loader first!")); return; } diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 540ee2fd..9dd8f737 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -68,7 +68,7 @@ void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) { auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); - m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoader() }); + m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoaders() }); } void ListModel::performPaginatedSearch() @@ -76,7 +76,7 @@ void ListModel::performPaginatedSearch() auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); m_parent->apiProvider()->searchMods( - this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() }); + this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() }); } void ListModel::refresh() diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 6dd3a453..ad36cf2f 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -175,7 +175,7 @@ void ModPage::updateModVersions(int prev_count) bool valid = false; for(auto& mcVer : m_filter->versions){ //NOTE: Flame doesn't care about loader, so passing it changes nothing. - if (validateVersion(version, mcVer.toString(), packProfile->getModLoader())) { + if (validateVersion(version, mcVer.toString(), packProfile->getModLoaders())) { valid = true; break; } diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index eb89b0e2..0e658a8d 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -37,7 +37,7 @@ class ModPage : public QWidget, public BasePage { void retranslate() override; auto shouldDisplay() const -> bool override = 0; - virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool = 0; + virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool = 0; auto apiProvider() const -> const ModAPI* { return api.get(); }; auto getFilter() const -> const std::shared_ptr { return m_filter; } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 70759994..1c160fd4 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -61,9 +61,9 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected); } -auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool +auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool { - Q_UNUSED(loader); + Q_UNUSED(loaders); return ver.mcVersion.contains(mineVer); } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 27cbdb8c..86e1a17b 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -55,7 +55,7 @@ class FlameModPage : public ModPage { inline auto debugName() const -> QString override { return "Flame"; } inline auto metaEntryBase() const -> QString override { return "FlameMods"; }; - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool override; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override; auto shouldDisplay() const -> bool override; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp index d3a1f859..0b81ea93 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp @@ -61,9 +61,9 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instan connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onModSelected); } -auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool +auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool { - auto loaderStrings = ModrinthAPI::getModLoaderStrings(loader); + auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders); auto loaderCompatible = false; for (auto remoteLoader : ver.loaders) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h index b1e72bfe..c39acaa0 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h @@ -55,7 +55,7 @@ class ModrinthModPage : public ModPage { inline auto debugName() const -> QString override { return "Modrinth"; } inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; }; - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool override; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override; auto shouldDisplay() const -> bool override; }; From 36045a8b0aa5c99e8520a39e6cc372ab9b549668 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 19 May 2022 12:35:44 +0200 Subject: [PATCH 425/605] chore: improve readability Co-authored-by: flow --- launcher/modplatform/modrinth/ModrinthAPI.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 39f6c49a..79bc5175 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -33,12 +33,12 @@ class ModrinthAPI : public NetworkModAPI { QStringList l; for (auto loader : {Forge, Fabric, Quilt}) { - if (types & loader || types == Unspecified) + if ((types & loader) || types == Unspecified) { l << ModAPI::getModLoaderString(loader); } } - if (types & Quilt && ~types & Fabric) // Add Fabric if Quilt is in use, if Fabric isn't already there + if ((types & Quilt) && (~types & Fabric)) // Add Fabric if Quilt is in use, if Fabric isn't already there l << ModAPI::getModLoaderString(Fabric); return l; } @@ -98,7 +98,7 @@ class ModrinthAPI : public NetworkModAPI { inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool { - return loaders == Unspecified || loaders & (Forge | Fabric | Quilt); + return (loaders == Unspecified) || (loaders & (Forge | Fabric | Quilt)); } }; From 97a83c9b7a72d37218acfbf5c325245eab0b5b23 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sun, 1 May 2022 18:12:21 +0100 Subject: [PATCH 426/605] ATLauncher: Avoid downloading Forge twice for older packs This resolves a quirk where Forge would still be downloaded for use as a jarmod, even when we detected Forge as a component. --- launcher/modplatform/atlauncher/ATLPackInstallTask.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 9dcb3504..991d737c 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -574,8 +574,6 @@ void PackInstallTask::downloadMods() jobPtr->addNetAction(dl); auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); - qDebug() << "Will download" << url << "to" << path; - modsToCopy[entry->getFullPath()] = path; if(mod.type == ModType::Forge) { auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge"); @@ -597,6 +595,10 @@ void PackInstallTask::downloadMods() qDebug() << "Jarmod: " + path; jarmods.push_back(path); } + + // Download after Forge handling, to avoid downloading Forge twice. + qDebug() << "Will download" << url << "to" << path; + modsToCopy[entry->getFullPath()] = path; } } From c329730de848f9ecf864aa4edbbc650faad7f21a Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sun, 1 May 2022 19:32:34 +0100 Subject: [PATCH 427/605] ATLauncher: Install LiteLoader as a component where possible --- .../atlauncher/ATLPackInstallTask.cpp | 91 ++++++++++++++++--- 1 file changed, 80 insertions(+), 11 deletions(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 991d737c..e9e3b872 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield - * Copyright 2021 Petr Mrazek + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "ATLPackInstallTask.h" @@ -305,7 +324,55 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared auto f = std::make_shared(); f->name = m_pack + " " + m_version_name + " (libraries)"; + const static QMap liteLoaderMap = { + { "61179803bcd5fb7790789b790908663d", "1.12-SNAPSHOT" }, + { "1420785ecbfed5aff4a586c5c9dd97eb", "1.12.2-SNAPSHOT" }, + { "073f68e2fcb518b91fd0d99462441714", "1.6.2_03" }, + { "10a15b52fc59b1bfb9c05b56de1097d6", "1.6.2_02" }, + { "b52f90f08303edd3d4c374e268a5acf1", "1.6.2_04" }, + { "ea747e24e03e24b7cad5bc8a246e0319", "1.6.2_01" }, + { "55785ccc82c07ff0ba038fe24be63ea2", "1.7.10_01" }, + { "63ada46e033d0cb6782bada09ad5ca4e", "1.7.10_04" }, + { "7983e4b28217c9ae8569074388409c86", "1.7.10_03" }, + { "c09882458d74fe0697c7681b8993097e", "1.7.10_02" }, + { "db7235aefd407ac1fde09a7baba50839", "1.7.10_00" }, + { "6e9028816027f53957bd8fcdfabae064", "1.8" }, + { "5e732dc446f9fe2abe5f9decaec40cde", "1.10-SNAPSHOT" }, + { "3a98b5ed95810bf164e71c1a53be568d", "1.11.2-SNAPSHOT" }, + { "ba8e6285966d7d988a96496f48cbddaa", "1.8.9-SNAPSHOT" }, + { "8524af3ac3325a82444cc75ae6e9112f", "1.11-SNAPSHOT" }, + { "53639d52340479ccf206a04f5e16606f", "1.5.2_01" }, + { "1fcdcf66ce0a0806b7ad8686afdce3f7", "1.6.4_00" }, + { "531c116f71ae2b11033f9a11a0f8e668", "1.6.4_01" }, + { "4009eeb99c9068f608d3483a6439af88", "1.7.2_03" }, + { "66f343354b8417abce1a10d557d2c6e9", "1.7.2_04" }, + { "ab554c21f28fbc4ae9b098bcb5f4cceb", "1.7.2_05" }, + { "e1d76a05a3723920e2f80a5e66c45f16", "1.7.2_02" }, + { "00318cb0c787934d523f63cdfe8ddde4", "1.9-SNAPSHOT" }, + { "986fd1ee9525cb0dcab7609401cef754", "1.9.4-SNAPSHOT" }, + { "571ad5e6edd5ff40259570c9be588bb5", "1.9.4" }, + { "1cdd72f7232e45551f16cc8ffd27ccf3", "1.10.2-SNAPSHOT" }, + { "8a7c21f32d77ee08b393dd3921ced8eb", "1.10.2" }, + { "b9bef8abc8dc309069aeba6fbbe58980", "1.12.1-SNAPSHOT" } + }; + for(const auto & lib : m_version.libraries) { + // If the library is LiteLoader, we need to ignore it and handle it separately. + if (liteLoaderMap.contains(lib.md5)) { + auto vlist = APPLICATION->metadataIndex()->get("com.mumfrey.liteloader"); + if (vlist) { + if (!vlist->isLoaded()) + vlist->load(Net::Mode::Online); + + auto ver = vlist->getVersion(liteLoaderMap.value(lib.md5)); + if (ver) { + ver->load(Net::Mode::Online); + componentsToInstall.insert("com.mumfrey.liteloader", ver); + continue; + } + } + } + auto libName = detectLibrary(lib); GradleSpecifier libSpecifier(libName); @@ -579,6 +646,8 @@ void PackInstallTask::downloadMods() auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge"); if(vlist) { + if (!vlist->isLoaded()) + vlist->load(Net::Mode::Online); auto ver = vlist->getVersion(mod.version); if(ver) { ver->load(Net::Mode::Online); From f5f59203a203318371fbc5257234b8c2c5eeb300 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sun, 1 May 2022 22:42:29 +0100 Subject: [PATCH 428/605] ATLauncher: Reduce boilerplate code for fetching versions --- .../atlauncher/ATLPackInstallTask.cpp | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index e9e3b872..4b8b8eb0 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -58,6 +58,8 @@ namespace ATLauncher { +static Meta::VersionPtr getComponentVersion(const QString& uid, const QString& version); + PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString pack, QString version) { m_support = support; @@ -115,19 +117,11 @@ void PackInstallTask::onDownloadSucceeded() } m_version = version; - auto vlist = APPLICATION->metadataIndex()->get("net.minecraft"); - if(!vlist) - { - emitFailed(tr("Failed to get local metadata index for %1").arg("net.minecraft")); - return; - } - - auto ver = vlist->getVersion(m_version.minecraft); + auto ver = getComponentVersion("net.minecraft", m_version.minecraft); if (!ver) { - emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft").arg(m_version.minecraft)); + emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft", m_version.minecraft)); return; } - ver->load(Net::Mode::Online); minecraftVersion = ver; if(m_version.noConfigs) { @@ -359,17 +353,10 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared for(const auto & lib : m_version.libraries) { // If the library is LiteLoader, we need to ignore it and handle it separately. if (liteLoaderMap.contains(lib.md5)) { - auto vlist = APPLICATION->metadataIndex()->get("com.mumfrey.liteloader"); - if (vlist) { - if (!vlist->isLoaded()) - vlist->load(Net::Mode::Online); - - auto ver = vlist->getVersion(liteLoaderMap.value(lib.md5)); - if (ver) { - ver->load(Net::Mode::Online); - componentsToInstall.insert("com.mumfrey.liteloader", ver); - continue; - } + auto ver = getComponentVersion("com.mumfrey.liteloader", liteLoaderMap.value(lib.md5)); + if (ver) { + componentsToInstall.insert("com.mumfrey.liteloader", ver); + continue; } } @@ -643,17 +630,10 @@ void PackInstallTask::downloadMods() auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); if(mod.type == ModType::Forge) { - auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge"); - if(vlist) - { - if (!vlist->isLoaded()) - vlist->load(Net::Mode::Online); - auto ver = vlist->getVersion(mod.version); - if(ver) { - ver->load(Net::Mode::Online); - componentsToInstall.insert("net.minecraftforge", ver); - continue; - } + auto ver = getComponentVersion("net.minecraftforge", mod.version); + if (ver) { + componentsToInstall.insert("net.minecraftforge", ver); + continue; } qDebug() << "Jarmod: " + path; @@ -850,4 +830,23 @@ void PackInstallTask::install() emitSucceeded(); } +static Meta::VersionPtr getComponentVersion(const QString& uid, const QString& version) +{ + auto vlist = APPLICATION->metadataIndex()->get(uid); + if (!vlist) + return {}; + + if (!vlist->isLoaded()) + vlist->load(Net::Mode::Online); + + auto ver = vlist->getVersion(version); + if (!ver) + return {}; + + if (!ver->isLoaded()) + ver->load(Net::Mode::Online); + + return ver; +} + } From 188c5aaa356323392be1100d74f62d70ab298695 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Tue, 17 May 2022 18:43:35 +0100 Subject: [PATCH 429/605] Launch: Match Vanilla launcher version string behaviour This removes a means of profiling users. --- launcher/minecraft/MinecraftInstance.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index e20dc24c..61326fac 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * 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 @@ -487,9 +488,8 @@ QStringList MinecraftInstance::processMinecraftArgs( } } - // blatant self-promotion. - token_mapping["profile_name"] = token_mapping["version_name"] = BuildConfig.LAUNCHER_NAME; - + token_mapping["profile_name"] = name(); + token_mapping["version_name"] = profile->getMinecraftVersion(); token_mapping["version_type"] = profile->getMinecraftVersionType(); QString absRootDir = QDir(gameRoot()).absolutePath(); From 96f16069a93afa320de174e740bc6b915e9a1103 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Tue, 17 May 2022 21:03:15 +0100 Subject: [PATCH 430/605] Launch: Apply the Minecraft version correctly It was previously using a deprecated field. --- launcher/minecraft/VersionFile.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/VersionFile.cpp b/launcher/minecraft/VersionFile.cpp index 9db30ba2..f242fbe7 100644 --- a/launcher/minecraft/VersionFile.cpp +++ b/launcher/minecraft/VersionFile.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * 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 @@ -55,7 +56,7 @@ void VersionFile::applyTo(LaunchProfile *profile) // Only real Minecraft can set those. Don't let anything override them. if (isMinecraftVersion(uid)) { - profile->applyMinecraftVersion(minecraftVersion); + profile->applyMinecraftVersion(version); profile->applyMinecraftVersionType(type); // HACK: ignore assets from other version files than Minecraft // workaround for stupid assets issue caused by amazon: From 2847cefff701dad137cd04f628c76a9282d04a83 Mon Sep 17 00:00:00 2001 From: dada513 Date: Fri, 20 May 2022 19:56:27 +0200 Subject: [PATCH 431/605] Add cursefrog key override --- launcher/Application.cpp | 11 +++++ launcher/Application.h | 1 + launcher/net/Download.cpp | 3 +- launcher/ui/pages/global/APIPage.cpp | 4 ++ launcher/ui/pages/global/APIPage.ui | 64 ++++++++++++++++++++++------ 5 files changed, 68 insertions(+), 15 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index dc8a7b0d..ce62c41a 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -679,6 +679,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Custom MSA credentials m_settings->registerSetting("MSAClientIDOverride", ""); + m_settings->registerSetting("CFKeyOverride", ""); // Init page provider { @@ -1508,3 +1509,13 @@ QString Application::getMSAClientID() return BuildConfig.MSA_CLIENT_ID; } + +QString Application::getCurseKey() +{ + QString keyOverride = m_settings->get("CFKeyOverride").toString(); + if (!keyOverride.isEmpty()) { + return keyOverride; + } + + return BuildConfig.CURSEFORGE_API_KEY; +} diff --git a/launcher/Application.h b/launcher/Application.h index 172321c0..3129b4fb 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -155,6 +155,7 @@ public: QString getJarsPath(); QString getMSAClientID(); + QString getCurseKey(); /// this is the root of the 'installation'. Used for automatic updates const QString &root() { diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 65cc8f67..7a401609 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -25,6 +25,7 @@ #include "MetaCacheSink.h" #include "BuildConfig.h" +#include "Application.h" namespace Net { @@ -96,7 +97,7 @@ void Download::startImpl() request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); if (request.url().host().contains("api.curseforge.com")) { - request.setRawHeader("x-api-key", BuildConfig.CURSEFORGE_API_KEY.toUtf8()); + request.setRawHeader("x-api-key", APPLICATION->getCurseKey().toUtf8()); }; QNetworkReply* rep = m_network->get(request); diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 287eb74f..8b806bcf 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -70,6 +70,8 @@ void APIPage::loadSettings() ui->urlChoices->setCurrentText(pastebinURL); QString msaClientID = s->get("MSAClientIDOverride").toString(); ui->msaClientID->setText(msaClientID); + QString curseKey = s->get("CFKeyOverride").toString(); + ui->curseKey->setText(curseKey); } void APIPage::applySettings() @@ -79,6 +81,8 @@ void APIPage::applySettings() s->set("PastebinURL", pastebinURL); QString msaClientID = ui->msaClientID->text(); s->set("MSAClientIDOverride", msaClientID); + QString curseKey = ui->curseKey->text(); + s->set("CFKeyOverride", curseKey); } bool APIPage::apply() diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index acde9aef..eaa44c88 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -6,8 +6,8 @@ 0 0 - 491 - 474 + 603 + 530 @@ -148,17 +148,56 @@ - - - Qt::Vertical + + + true - - - 20 - 40 - + + &CurseForge Core API - + + + + + Qt::Horizontal + + + + + + + Note: you probably don't need to set this if CurseForge already works. + + + + + + + true + + + (Default) + + + + + + + Enter a custom API Key for CurseForge here. + + + Qt::RichText + + + true + + + true + + + + + @@ -166,9 +205,6 @@ - - tabWidget - From 6afe59e76b6a5d44b8706e8e030ecd0396dc8801 Mon Sep 17 00:00:00 2001 From: timoreo Date: Fri, 20 May 2022 21:19:19 +0200 Subject: [PATCH 432/605] Very Temporary Fix for curseforge --- .../modplatform/flame/FileResolvingTask.cpp | 16 +- launcher/modplatform/flame/PackManifest.cpp | 16 +- .../ui/pages/modplatform/flame/FlamePage.ui | 179 +++++++++--------- 3 files changed, 118 insertions(+), 93 deletions(-) diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 95924a68..0deb99c4 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -31,7 +31,21 @@ void Flame::FileResolvingTask::netJobFinished() for (auto& bytes : results) { auto& out = m_toProcess.files[index]; try { - failed &= (!out.parseFromBytes(bytes)); + bool fail = (!out.parseFromBytes(bytes)); + if(fail){ + //failed :( probably disabled mod, try to add to the list + auto doc = Json::requireDocument(bytes); + if (!doc.isObject()) { + throw JSONValidationError(QString("data is not an object? that's not supposed to happen")); + } + auto obj = Json::ensureObject(doc.object(), "data"); + //FIXME : HACK, MAY NOT WORK FOR LONG + out.url = QUrl(QString("https://media.forgecdn.net/files/%1/%2/%3") + .arg(QString::number(QString::number(out.fileId).leftRef(4).toInt()) + ,QString::number(QString::number(out.fileId).rightRef(3).toInt()) + ,QUrl::toPercentEncoding(out.fileName)), QUrl::TolerantMode); + } + failed &= fail; } catch (const JSONValidationError& e) { qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:"; qCritical() << e.cause(); diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index e4f90c1a..c78783a0 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -71,11 +71,6 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes) fileName = Json::requireString(obj, "fileName"); - QString rawUrl = Json::requireString(obj, "downloadUrl"); - url = QUrl(rawUrl, QUrl::TolerantMode); - if (!url.isValid()) { - throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); - } // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience // It is also optional type = File::Type::SingleFile; @@ -87,7 +82,16 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes) // this is probably a mod, dunno what else could modpacks download targetFolder = "mods"; } - + if(!obj.contains("downloadUrl") || obj["downloadUrl"].isNull() || !obj["downloadUrl"].isString() || obj["downloadUrl"].toString().isEmpty()){ + //either there somehow is an emtpy string as a link, or it's null either way it's invalid + //soft failing + return false; + } + QString rawUrl = Json::requireString(obj, "downloadUrl"); + url = QUrl(rawUrl, QUrl::TolerantMode); + if (!url.isValid()) { + throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); + } resolved = true; return true; } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index 6d8d8e10..b337d672 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -1,90 +1,97 @@ - FlamePage - - - - 0 - 0 - 837 - 685 - - - - - - - - - - 48 - 48 - - - - Qt::ScrollBarAlwaysOff - - - true - - - - - - - true - - - true - - - - - - - - - - - - - - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - Search - - - - - - - Search and filter... - - - + FlamePage + + + + 0 + 0 + 1989 + 685 + + + + + + + Search + + + + + + + Search and filter... + + + + + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + + + true + + + true + + + - - - searchEdit - searchButton - packView - packDescription - sortByBox - versionSelectionBox - - - +
+ + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + WARNING !! Curseforge is very unreliable and low quality. Some mod authors have disabled the ability for third party apps (like polymc) to download the mods, you may need to manually download some mods + + + + + + + searchEdit + searchButton + packView + packDescription + sortByBox + versionSelectionBox + + + From cbc8c1aed63e9cd106b468ae720aa650beec646a Mon Sep 17 00:00:00 2001 From: Kenneth Chew <79120643+kthchew@users.noreply.github.com> Date: Fri, 20 May 2022 15:56:13 -0400 Subject: [PATCH 433/605] Use consistent naming scheme Co-authored-by: Sefa Eyeoglu --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ab30d45..d12f176c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -269,7 +269,7 @@ jobs: if: runner.os == 'Windows' uses: actions/upload-artifact@v3 with: - name: PolyMC-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}-Setup + name: PolyMC-${{ matrix.name }}-Setup-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC-Setup.exe - name: Upload binary tarball (Linux) From 30b56dbcbd3bb7d61210405a469c7efb28581904 Mon Sep 17 00:00:00 2001 From: timoreo Date: Fri, 20 May 2022 22:00:38 +0200 Subject: [PATCH 434/605] Port temp fix to mods too --- launcher/modplatform/flame/FlameModIndex.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index ba0824cf..9846b156 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -56,8 +56,15 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, file.fileId = Json::requireInteger(obj, "id"); file.date = Json::requireString(obj, "fileDate"); file.version = Json::requireString(obj, "displayName"); - file.downloadUrl = Json::requireString(obj, "downloadUrl"); file.fileName = Json::requireString(obj, "fileName"); + file.downloadUrl = Json::ensureString(obj, "downloadUrl", ""); + if(file.downloadUrl.isEmpty()){ + //FIXME : HACK, MAY NOT WORK FOR LONG + file.downloadUrl = QString("https://media.forgecdn.net/files/%1/%2/%3") + .arg(QString::number(QString::number(file.fileId.toInt()).leftRef(4).toInt()) + ,QString::number(QString::number(file.fileId.toInt()).rightRef(3).toInt()) + ,QUrl::toPercentEncoding(file.fileName)); + } unsortedVersions.append(file); } From 6542f5f15af31e493c9b46afb3a5b4b330cc9cee Mon Sep 17 00:00:00 2001 From: timoreo Date: Fri, 20 May 2022 22:06:36 +0200 Subject: [PATCH 435/605] Apply suggestions --- launcher/modplatform/flame/PackManifest.cpp | 5 +- .../ui/pages/modplatform/flame/FlamePage.ui | 69 ++++++++++--------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index c78783a0..3217a756 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -82,12 +82,13 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes) // this is probably a mod, dunno what else could modpacks download targetFolder = "mods"; } - if(!obj.contains("downloadUrl") || obj["downloadUrl"].isNull() || !obj["downloadUrl"].isString() || obj["downloadUrl"].toString().isEmpty()){ + QString rawUrl = Json::ensureString(obj, "downloadUrl"); + + if(rawUrl.isEmpty()){ //either there somehow is an emtpy string as a link, or it's null either way it's invalid //soft failing return false; } - QString rawUrl = Json::requireString(obj, "downloadUrl"); url = QUrl(rawUrl, QUrl::TolerantMode); if (!url.isValid()) { throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index b337d672..4c7a6495 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -6,26 +6,39 @@ 0 0 - 1989 + 2445 685 - - - - Search - - + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + - + Search and filter... - + @@ -55,30 +68,22 @@ - - - - - - - - - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - + + - WARNING !! Curseforge is very unreliable and low quality. Some mod authors have disabled the ability for third party apps (like polymc) to download the mods, you may need to manually download some mods + Search + + + + + + + + true + + + + WARNING: CurseForge's API is very unreliable and low quality. Also, some mod authors have disabled the ability for third party apps (like PolyMC) to download their mods. As such, you may need to manually download some mods to be able to use the modpack. From 3b4b34b3695e655f591347754a724804bea96d71 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 20 May 2022 22:46:35 +0200 Subject: [PATCH 436/605] fix(ui): make CF and MR modpack dialogs more consistent --- .../ui/pages/modplatform/flame/FlamePage.ui | 108 ++++++++++-------- .../modplatform/modrinth/ModrinthPage.ui | 7 +- 2 files changed, 63 insertions(+), 52 deletions(-) diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index 4c7a6495..9fab9773 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -6,41 +6,50 @@ 0 0 - 2445 - 685 + 800 + 600 - - - - - - - - - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - Search and filter... + + + + + true + + + + Note: CurseForge's API is very unreliable. CurseForge and some mod authors have disallowed downloading mods in third-party applications like PolyMC. As such, you may need to manually download some mods to be able to install a modpack. + + + Qt::AlignCenter + + + true - - - + + + + + + Search and filter... + + + + + + + Search + + + + + + + + Qt::ScrollBarAlwaysOff @@ -56,7 +65,7 @@ - + true @@ -68,30 +77,29 @@ - - - - Search - - - - - - - - true - - - - WARNING: CurseForge's API is very unreliable and low quality. Also, some mod authors have disabled the ability for third party apps (like PolyMC) to download their mods. As such, you may need to manually download some mods to be able to use the modpack. - - + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + - searchEdit - searchButton packView packDescription sortByBox diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 4fb59cdf..ae9556ed 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -6,8 +6,8 @@ 0 0 - 837 - 685 + 800 + 600 @@ -24,6 +24,9 @@ Qt::AlignCenter + + true + From 2bc6da038dea701699ba9fc46eb68b3a74d5f488 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 20 May 2022 17:09:26 -0400 Subject: [PATCH 437/605] Add installer to release workflow --- .github/workflows/trigger_release.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index ff0d2b3f..91cd0474 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -43,10 +43,12 @@ jobs: for d in PolyMC-Windows-*; do cd "${d}" || continue ARCH="$(echo -n ${d} | cut -d '-' -f 3)" + INST="$(echo -n ${d} | grep -o Setup || true)" PORT="$(echo -n ${d} | grep -o Portable || true)" NAME="PolyMC-Windows-${ARCH}" test -z "${PORT}" || NAME="${NAME}-Portable" - zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * + test -z "${INST}" || mv PolyMC-*.exe ../${NAME}-Setup-${{ env.VERSION }}.exe + test -n "${INST}" || zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * cd .. done @@ -66,7 +68,9 @@ jobs: PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage PolyMC-Windows-i686-${{ env.VERSION }}.zip PolyMC-Windows-i686-Portable-${{ env.VERSION }}.zip + PolyMC-Windows-i686-Setup-${{ env.VERSION }}.exe PolyMC-Windows-x86_64-${{ env.VERSION }}.zip PolyMC-Windows-x86_64-Portable-${{ env.VERSION }}.zip + PolyMC-Windows-x86_64-Setup-${{ env.VERSION }}.exe PolyMC-macOS-${{ env.VERSION }}.tar.gz PolyMC-${{ env.VERSION }}.tar.gz From 12cadf3af0a4e3a01330283fae2d6267d3c3f525 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 20 May 2022 17:09:42 -0400 Subject: [PATCH 438/605] Add `/NoUninstaller` parameter for Windows installer --- program_info/win_install.nsi | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi index ce13b8b0..2d3f0f57 100644 --- a/program_info/win_install.nsi +++ b/program_info/win_install.nsi @@ -1,4 +1,5 @@ !include "FileFunc.nsh" +!include "LogicLib.nsh" !include "MUI2.nsh" Unicode true @@ -119,20 +120,24 @@ Section "PolyMC" WriteRegStr HKCU Software\PolyMC "InstallDir" "$INSTDIR" ; Write the uninstall keys for Windows - !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" - WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "PolyMC" - WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\polymc.exe" - WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe"' - WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S' - WriteRegStr HKCU "${UNINST_KEY}" "InstallLocation" "$INSTDIR" - WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "PolyMC Contributors" - WriteRegStr HKCU "${UNINST_KEY}" "ProductVersion" "${VERSION}" - ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 - IntFmt $0 "0x%08X" $0 - WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0" - WriteRegDWORD HKCU "${UNINST_KEY}" "NoModify" 1 - WriteRegDWORD HKCU "${UNINST_KEY}" "NoRepair" 1 - WriteUninstaller "$INSTDIR\uninstall.exe" + ${GetParameters} $R0 + ${GetOptions} $R0 "/NoUninstaller" $R1 + ${If} ${Errors} + !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "PolyMC" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\polymc.exe" + WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe"' + WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S' + WriteRegStr HKCU "${UNINST_KEY}" "InstallLocation" "$INSTDIR" + WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "PolyMC Contributors" + WriteRegStr HKCU "${UNINST_KEY}" "ProductVersion" "${VERSION}" + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0" + WriteRegDWORD HKCU "${UNINST_KEY}" "NoModify" 1 + WriteRegDWORD HKCU "${UNINST_KEY}" "NoRepair" 1 + WriteUninstaller "$INSTDIR\uninstall.exe" + ${EndIf} SectionEnd From cdd83c279cafdacee6c863d7fb0ae94a6bf34e3e Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 20 May 2022 17:12:08 -0400 Subject: [PATCH 439/605] Remove portable option in Windows installer --- .github/workflows/build.yml | 2 +- program_info/win_install.nsi | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d12f176c..53db7d76 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -195,7 +195,7 @@ jobs: if: runner.os == 'Windows' shell: msys2 {0} run: | - cd ${{ env.INSTALL_PORTABLE_DIR }} + cd ${{ env.INSTALL_DIR }} makensis -NOCD "-DVERSION=${{ env.VERSION }}" "-DMUI_ICON=${{ github.workspace }}/program_info/polymc.ico" "-XOutFile ${{ github.workspace }}/PolyMC-Setup.exe" "${{ github.workspace }}/program_info/win_install.nsi" - name: Package (Linux) diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi index 2d3f0f57..a47d4ae3 100644 --- a/program_info/win_install.nsi +++ b/program_info/win_install.nsi @@ -147,13 +147,6 @@ Section "Start Menu Shortcuts" SectionEnd -Section /o "Portable" - - SetOutPath $INSTDIR - File "portable.txt" - -SectionEnd - ;-------------------------------- ; Uninstaller From 1ec7878c07a8ba7d04a9fe860761872547fd5a0d Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 20 May 2022 17:22:30 -0400 Subject: [PATCH 440/605] Add `/NoShortcuts` parameter for Windows installer --- program_info/win_install.nsi | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi index a47d4ae3..7d48ccf2 100644 --- a/program_info/win_install.nsi +++ b/program_info/win_install.nsi @@ -141,7 +141,7 @@ Section "PolyMC" SectionEnd -Section "Start Menu Shortcuts" +Section "Start Menu Shortcuts" SHORTCUTS CreateShortcut "$SMPROGRAMS\PolyMC.lnk" "$INSTDIR\polymc.exe" "" "$INSTDIR\polymc.exe" 0 @@ -219,3 +219,15 @@ Section "Uninstall" RMDir "$INSTDIR" SectionEnd + +;-------------------------------- + +; Extra command line parameters + +Function .onInit +${GetParameters} $R0 +${GetOptions} $R0 "/NoShortcuts" $R1 +${IfNot} ${Errors} + !insertmacro UnselectSection ${SHORTCUTS} +${EndIf} +FunctionEnd From 3cab0e69f1c299e385330d97ab5159a0c8c904ec Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 20 May 2022 17:23:11 -0400 Subject: [PATCH 441/605] Fix default install location --- program_info/win_install.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi index 7d48ccf2..4ca4de1a 100644 --- a/program_info/win_install.nsi +++ b/program_info/win_install.nsi @@ -5,7 +5,7 @@ Unicode true Name "PolyMC" -InstallDir "$LOCALAPPDATA\PolyMC" +InstallDir "$LOCALAPPDATA\Programs\PolyMC" InstallDirRegKey HKCU "Software\PolyMC" "InstallDir" RequestExecutionLevel user From c04adf74521127bc50c67f3e2ddd1edfe2330358 Mon Sep 17 00:00:00 2001 From: timoreo Date: Sat, 21 May 2022 08:31:07 +0200 Subject: [PATCH 442/605] Do the url trick on initial modpack download too --- launcher/modplatform/flame/FlamePackIndex.cpp | 10 +++++++++- launcher/modplatform/flame/FlamePackIndex.h | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index ac24c647..6d48a3bf 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -65,7 +65,15 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) // pick the latest version supported file.mcVersion = versionArray[0].toString(); file.version = Json::requireString(version, "displayName"); - file.downloadUrl = Json::requireString(version, "downloadUrl"); + file.fileName = Json::requireString(version, "fileName"); + file.downloadUrl = Json::ensureString(version, "downloadUrl"); + if(file.downloadUrl.isEmpty()){ + //FIXME : HACK, MAY NOT WORK FOR LONG + file.downloadUrl = QString("https://media.forgecdn.net/files/%1/%2/%3") + .arg(QString::number(QString::number(file.fileId).leftRef(4).toInt()) + ,QString::number(QString::number(file.fileId).rightRef(3).toInt()) + ,QUrl::toPercentEncoding(file.fileName)); + } unsortedVersions.append(file); } diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index 7ffa29c3..a8bb15be 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -18,6 +18,7 @@ struct IndexedVersion { QString version; QString mcVersion; QString downloadUrl; + QString fileName; }; struct IndexedPack From 613f2fc4479dbfc3cf3149b0a2ceb0df1f26095f Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 22 Apr 2022 13:20:31 -0300 Subject: [PATCH 443/605] feat: allow deselecting mods from the mod confirmation dialog This adds a checkbox to each mod on the dialog that shows up when confirming the mods to download, so you can deselect some of those if you want to. --- launcher/ui/dialogs/ModDownloadDialog.cpp | 18 ++--- launcher/ui/dialogs/ReviewMessageBox.cpp | 27 +++++++- launcher/ui/dialogs/ReviewMessageBox.h | 12 +++- launcher/ui/dialogs/ReviewMessageBox.ui | 81 ++++++++--------------- 4 files changed, 71 insertions(+), 67 deletions(-) diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 305e85c0..436f51f9 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -77,18 +77,20 @@ void ModDownloadDialog::confirm() auto keys = modTask.keys(); keys.sort(Qt::CaseInsensitive); - auto confirm_dialog = ReviewMessageBox::create( - this, - tr("Confirm mods to download") - ); + auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm mods to download")); - for(auto& task : keys){ - confirm_dialog->appendMod(task, modTask.find(task).value()->getFilename()); + for (auto& task : keys) { + confirm_dialog->appendMod({ task, modTask.find(task).value()->getFilename() }); } - connect(confirm_dialog, &QDialog::accepted, this, &ModDownloadDialog::accept); + if (confirm_dialog->exec()) { + auto deselected = confirm_dialog->deselectedMods(); + for (auto name : deselected) { + modTask.remove(name); + } - confirm_dialog->open(); + this->accept(); + } } void ModDownloadDialog::accept() diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index 2bfd02e0..c92234a4 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -5,6 +5,9 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QStrin : QDialog(parent), ui(new Ui::ReviewMessageBox) { ui->setupUi(this); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject); } ReviewMessageBox::~ReviewMessageBox() @@ -17,15 +20,33 @@ auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon) return new ReviewMessageBox(parent, title, icon); } -void ReviewMessageBox::appendMod(const QString& name, const QString& filename) +void ReviewMessageBox::appendMod(ModInformation&& info) { auto itemTop = new QTreeWidgetItem(ui->modTreeWidget); - itemTop->setText(0, name); + itemTop->setCheckState(0, Qt::CheckState::Checked); + itemTop->setText(0, info.name); auto filenameItem = new QTreeWidgetItem(itemTop); - filenameItem->setText(0, tr("Filename: %1").arg(filename)); + filenameItem->setText(0, tr("Filename: %1").arg(info.filename)); itemTop->insertChildren(0, { filenameItem }); ui->modTreeWidget->addTopLevelItem(itemTop); } + +auto ReviewMessageBox::deselectedMods() -> QStringList +{ + QStringList list; + + auto* item = ui->modTreeWidget->topLevelItem(0); + + for (int i = 0; item != nullptr; ++i) { + if (item->checkState(0) == Qt::CheckState::Unchecked) { + list.append(item->text(0)); + } + + item = ui->modTreeWidget->topLevelItem(i); + } + + return list; +} diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h index 48742cd9..9cfa679a 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.h +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -6,17 +6,23 @@ namespace Ui { class ReviewMessageBox; } -class ReviewMessageBox final : public QDialog { +class ReviewMessageBox : public QDialog { Q_OBJECT public: static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*; - void appendMod(const QString& name, const QString& filename); + using ModInformation = struct { + QString name; + QString filename; + }; + + void appendMod(ModInformation&& info); + auto deselectedMods() -> QStringList; ~ReviewMessageBox(); - private: + protected: ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon); Ui::ReviewMessageBox* ui; diff --git a/launcher/ui/dialogs/ReviewMessageBox.ui b/launcher/ui/dialogs/ReviewMessageBox.ui index d04f3b3f..ab3bcc2f 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.ui +++ b/launcher/ui/dialogs/ReviewMessageBox.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 300 + 500 + 350 @@ -20,24 +20,7 @@ true - - - - You're about to download the following mods: - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - true @@ -58,41 +41,33 @@ + + + + You're about to download the following mods: + + + + + + + + + Only mods with a check will be downloaded! + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + - - - buttonBox - accepted() - ReviewMessageBox - accept() - - - 200 - 265 - - - 199 - 149 - - - - - buttonBox - rejected() - ReviewMessageBox - reject() - - - 200 - 265 - - - 199 - 149 - - - - + From 8f2c485c926e53f8b31f420f3d5caec090982498 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 28 Apr 2022 20:14:03 -0300 Subject: [PATCH 444/605] feat(ui): make selected mods in downloader bold with underline Makes it easier to find which mods are selected in case you want to change those. --- launcher/ui/dialogs/ModDownloadDialog.cpp | 6 +++ launcher/ui/dialogs/ModDownloadDialog.h | 3 +- launcher/ui/pages/modplatform/ModModel.cpp | 55 ++++++++++++++-------- launcher/ui/pages/modplatform/ModPage.h | 1 + 4 files changed, 44 insertions(+), 21 deletions(-) diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index 436f51f9..f01c9c07 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -134,6 +134,12 @@ bool ModDownloadDialog::isModSelected(const QString &name, const QString& filena return iter != modTask.end() && (iter.value()->getFilename() == filename); } +bool ModDownloadDialog::isModSelected(const QString &name) const +{ + auto iter = modTask.find(name); + return iter != modTask.end(); +} + ModDownloadDialog::~ModDownloadDialog() { } diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 782dc361..5c565ad3 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -32,6 +32,7 @@ public: void addSelectedMod(const QString & name = QString(), ModDownloadTask * task = nullptr); void removeSelectedMod(const QString & name = QString()); bool isModSelected(const QString & name, const QString & filename) const; + bool isModSelected(const QString & name) const; const QList getTasks(); const std::shared_ptr &mods; @@ -41,8 +42,6 @@ public slots: void accept() override; void reject() override; -//private slots: - private: Ui::ModDownloadDialog *ui = nullptr; PageContainer * m_container = nullptr; diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 540ee2fd..67d1de3e 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -38,27 +38,44 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant } ModPlatform::IndexedPack pack = modpacks.at(pos); - if (role == Qt::DisplayRole) { - return pack.name; - } else if (role == Qt::ToolTipRole) { - if (pack.description.length() > 100) { - // some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); - edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); - return edit; + switch (role) { + case Qt::DisplayRole: { + return pack.name; } - return pack.description; - } else if (role == Qt::DecorationRole) { - if (m_logoMap.contains(pack.logoName)) { - return (m_logoMap.value(pack.logoName)); + case Qt::ToolTipRole: { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); - return icon; - } else if (role == Qt::UserRole) { - QVariant v; - v.setValue(pack); - return v; + case Qt::DecorationRole: { + if (m_logoMap.contains(pack.logoName)) { + return (m_logoMap.value(pack.logoName)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + // un-const-ify this + ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); + return icon; + } + case Qt::UserRole: { + QVariant v; + v.setValue(pack); + return v; + } + case Qt::FontRole: { + QFont font; + if (m_parent->getDialog()->isModSelected(pack.name)) { + font.setBold(true); + font.setUnderline(true); + } + + return font; + } + default: + break; } return {}; diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index eb89b0e2..8ffc4a53 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -41,6 +41,7 @@ class ModPage : public QWidget, public BasePage { auto apiProvider() const -> const ModAPI* { return api.get(); }; auto getFilter() const -> const std::shared_ptr { return m_filter; } + auto getDialog() const -> const ModDownloadDialog* { return dialog; } auto getCurrent() -> ModPlatform::IndexedPack& { return current; } void updateModVersions(int prev_count = -1); From 166f8727121399f7604d25580ced39472e9a3034 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 1 May 2022 11:08:00 -0300 Subject: [PATCH 445/605] fix: various issues with ProgressDialog and SequentialTasks - Fix aborting sequential tasks - Fix displaying wrong number of tasks concluded - Fix text cutting when the URL is too big --- launcher/tasks/SequentialTask.cpp | 14 ++- launcher/ui/dialogs/ProgressDialog.cpp | 91 ++++++++------------ launcher/ui/dialogs/ProgressDialog.ui | 6 ++ launcher/ui/pages/instance/ModFolderPage.cpp | 5 ++ 4 files changed, 58 insertions(+), 58 deletions(-) diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp index 1573e476..e7d58524 100644 --- a/launcher/tasks/SequentialTask.cpp +++ b/launcher/tasks/SequentialTask.cpp @@ -33,11 +33,17 @@ void SequentialTask::executeTask() bool SequentialTask::abort() { - bool succeeded = true; - for (auto& task : m_queue) { - if (!task->abort()) succeeded = false; + if(m_currentIndex == -1 || m_currentIndex >= m_queue.size()) { + m_queue.clear(); + return true; } + bool succeeded = m_queue[m_currentIndex]->abort(); + m_queue.clear(); + + if(succeeded) + emitAborted(); + return succeeded; } @@ -76,7 +82,7 @@ void SequentialTask::subTaskProgress(qint64 current, qint64 total) setProgress(0, 100); return; } - setProgress(m_currentIndex, m_queue.count()); + setProgress(m_currentIndex + 1, m_queue.count()); m_stepProgress = current; m_stepTotalProgress = total; diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index 648bd88b..e5226016 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -16,12 +16,12 @@ #include "ProgressDialog.h" #include "ui_ProgressDialog.h" -#include #include +#include #include "tasks/Task.h" -ProgressDialog::ProgressDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ProgressDialog) +ProgressDialog::ProgressDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ProgressDialog) { ui->setupUi(this); this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); @@ -44,6 +44,7 @@ void ProgressDialog::on_skipButton_clicked(bool checked) { Q_UNUSED(checked); task->abort(); + QDialog::reject(); } ProgressDialog::~ProgressDialog() @@ -53,24 +54,22 @@ ProgressDialog::~ProgressDialog() void ProgressDialog::updateSize() { - QSize qSize = QSize(480, minimumSizeHint().height()); + QSize qSize = QSize(480, minimumSizeHint().height()); resize(qSize); - setFixedSize(qSize); + setFixedSize(qSize); } -int ProgressDialog::execWithTask(Task *task) +int ProgressDialog::execWithTask(Task* task) { this->task = task; QDialog::DialogCode result; - if(!task) - { + if (!task) { qDebug() << "Programmer error: progress dialog created with null task."; return Accepted; } - if(handleImmediateResult(result)) - { + if (handleImmediateResult(result)) { return result; } @@ -78,58 +77,51 @@ int ProgressDialog::execWithTask(Task *task) connect(task, SIGNAL(started()), SLOT(onTaskStarted())); connect(task, SIGNAL(failed(QString)), SLOT(onTaskFailed(QString))); connect(task, SIGNAL(succeeded()), SLOT(onTaskSucceeded())); - connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString &))); + connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString&))); + connect(task, SIGNAL(stepStatus(QString)), SLOT(changeStatus(const QString&))); connect(task, SIGNAL(progress(qint64, qint64)), SLOT(changeProgress(qint64, qint64))); + connect(task, &Task::aborted, [this] { onTaskFailed(tr("Aborted by user")); }); + m_is_multi_step = task->isMultiStep(); - if(!m_is_multi_step){ + if (!m_is_multi_step) { ui->globalStatusLabel->setHidden(true); ui->globalProgressBar->setHidden(true); } // if this didn't connect to an already running task, invoke start - if(!task->isRunning()) - { + if (!task->isRunning()) { task->start(); } - if(task->isRunning()) - { + if (task->isRunning()) { changeProgress(task->getProgress(), task->getTotalProgress()); changeStatus(task->getStatus()); return QDialog::exec(); - } - else if(handleImmediateResult(result)) - { + } else if (handleImmediateResult(result)) { return result; - } - else - { + } else { return QDialog::Rejected; } } // TODO: only provide the unique_ptr overloads -int ProgressDialog::execWithTask(std::unique_ptr &&task) +int ProgressDialog::execWithTask(std::unique_ptr&& task) { connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); return execWithTask(task.release()); } -int ProgressDialog::execWithTask(std::unique_ptr &task) +int ProgressDialog::execWithTask(std::unique_ptr& task) { connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); return execWithTask(task.release()); } -bool ProgressDialog::handleImmediateResult(QDialog::DialogCode &result) +bool ProgressDialog::handleImmediateResult(QDialog::DialogCode& result) { - if(task->isFinished()) - { - if(task->wasSuccessful()) - { + if (task->isFinished()) { + if (task->wasSuccessful()) { result = QDialog::Accepted; - } - else - { + } else { result = QDialog::Rejected; } return true; @@ -137,14 +129,12 @@ bool ProgressDialog::handleImmediateResult(QDialog::DialogCode &result) return false; } -Task *ProgressDialog::getTask() +Task* ProgressDialog::getTask() { return task; } -void ProgressDialog::onTaskStarted() -{ -} +void ProgressDialog::onTaskStarted() {} void ProgressDialog::onTaskFailed(QString failure) { @@ -156,10 +146,11 @@ void ProgressDialog::onTaskSucceeded() accept(); } -void ProgressDialog::changeStatus(const QString &status) +void ProgressDialog::changeStatus(const QString& status) { + ui->globalStatusLabel->setText(task->getStatus()); ui->statusLabel->setText(task->getStepStatus()); - ui->globalStatusLabel->setText(status); + updateSize(); } @@ -168,27 +159,22 @@ void ProgressDialog::changeProgress(qint64 current, qint64 total) ui->globalProgressBar->setMaximum(total); ui->globalProgressBar->setValue(current); - if(!m_is_multi_step){ + if (!m_is_multi_step) { ui->taskProgressBar->setMaximum(total); ui->taskProgressBar->setValue(current); - } - else{ + } else { ui->taskProgressBar->setMaximum(task->getStepProgress()); ui->taskProgressBar->setValue(task->getStepTotalProgress()); } } -void ProgressDialog::keyPressEvent(QKeyEvent *e) +void ProgressDialog::keyPressEvent(QKeyEvent* e) { - if(ui->skipButton->isVisible()) - { - if (e->key() == Qt::Key_Escape) - { + if (ui->skipButton->isVisible()) { + if (e->key() == Qt::Key_Escape) { on_skipButton_clicked(true); return; - } - else if(e->key() == Qt::Key_Tab) - { + } else if (e->key() == Qt::Key_Tab) { ui->skipButton->setFocusPolicy(Qt::StrongFocus); ui->skipButton->setFocus(); ui->skipButton->setAutoDefault(true); @@ -199,14 +185,11 @@ void ProgressDialog::keyPressEvent(QKeyEvent *e) QDialog::keyPressEvent(e); } -void ProgressDialog::closeEvent(QCloseEvent *e) +void ProgressDialog::closeEvent(QCloseEvent* e) { - if (task && task->isRunning()) - { + if (task && task->isRunning()) { e->ignore(); - } - else - { + } else { QDialog::closeEvent(e); } } diff --git a/launcher/ui/dialogs/ProgressDialog.ui b/launcher/ui/dialogs/ProgressDialog.ui index bf119a78..34ab71e3 100644 --- a/launcher/ui/dialogs/ProgressDialog.ui +++ b/launcher/ui/dialogs/ProgressDialog.ui @@ -40,6 +40,12 @@
+ + + 0 + 0 + + Task Status... diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 8113fe85..cba25564 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -402,6 +402,10 @@ void ModFolderPage::on_actionInstall_mods_triggered() 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(); } @@ -411,6 +415,7 @@ void ModFolderPage::on_actionInstall_mods_triggered() for (auto task : mdownload.getTasks()) { tasks->addTask(task); } + ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(tasks); From 7c251efc473ee90069d1e87a056bde64f1d6fbf7 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Mon, 2 May 2022 20:27:20 +0100 Subject: [PATCH 446/605] ATLauncher: Display mod colours in optional mod dialog --- .../atlauncher/ATLPackInstallTask.cpp | 2 +- .../atlauncher/ATLPackInstallTask.h | 2 +- .../atlauncher/ATLPackManifest.cpp | 6 ++++++ .../modplatform/atlauncher/ATLPackManifest.h | 6 +++++- .../atlauncher/AtlOptionalModDialog.cpp | 21 +++++++++++++------ .../atlauncher/AtlOptionalModDialog.h | 6 ++++-- .../pages/modplatform/atlauncher/AtlPage.cpp | 5 +++-- .../ui/pages/modplatform/atlauncher/AtlPage.h | 2 +- 8 files changed, 36 insertions(+), 14 deletions(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 4b8b8eb0..90dc1365 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -556,7 +556,7 @@ void PackInstallTask::downloadMods() QVector selectedMods; if (!optionalMods.isEmpty()) { setStatus(tr("Selecting optional mods...")); - selectedMods = m_support->chooseOptionalMods(optionalMods); + selectedMods = m_support->chooseOptionalMods(m_version, optionalMods); } setStatus(tr("Downloading mods...")); diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index 783ec19b..6bc30689 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -37,7 +37,7 @@ public: /** * Requests a user interaction to select which optional mods should be installed. */ - virtual QVector chooseOptionalMods(QVector mods) = 0; + virtual QVector chooseOptionalMods(PackVersion version, QVector mods) = 0; /** * Requests a user interaction to select a component version from a given version list diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp index 40be6d53..a8f2711b 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp +++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -178,6 +178,7 @@ static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { p.depends.append(Json::requireString(depends)); } } + p.colour = Json::ensureString(obj, QString("colour"), ""); p.client = Json::ensureBoolean(obj, QString("client"), false); @@ -232,4 +233,9 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) auto configsObj = Json::requireObject(obj, "configs"); loadVersionConfigs(v.configs, configsObj); } + + auto colourObj = Json::ensureObject(obj, "colours"); + for (const auto &key : colourObj.keys()) { + v.colours[key] = Json::requireString(colourObj.value(key), "colour"); + } } diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h index 673f2f8b..2911107e 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -16,9 +16,10 @@ #pragma once +#include +#include #include #include -#include namespace ATLauncher { @@ -109,6 +110,7 @@ struct VersionMod bool library; QString group; QVector depends; + QString colour; bool client; @@ -134,6 +136,8 @@ struct PackVersion QVector libraries; QVector mods; VersionConfigs configs; + + QMap colours; }; void loadVersion(PackVersion & v, QJsonObject & obj); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index 26aa60af..aee5a78e 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -43,8 +43,11 @@ #include "modplatform/atlauncher/ATLShareCode.h" #include "Application.h" -AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, QVector mods) - : QAbstractListModel(parent), m_mods(mods) { +AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, ATLauncher::PackVersion version, QVector mods) + : QAbstractListModel(parent) + , m_version(version) + , m_mods(mods) +{ // fill mod index for (int i = 0; i < m_mods.size(); i++) { auto mod = m_mods.at(i); @@ -97,6 +100,11 @@ QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const return mod.description; } } + else if (role == Qt::ForegroundRole) { + if (!mod.colour.isEmpty() && m_version.colours.contains(mod.colour)) { + return QColor(QString("#%1").arg(m_version.colours[mod.colour])); + } + } else if (role == Qt::CheckStateRole) { if (index.column() == EnabledColumn) { return m_selection[mod.name] ? Qt::Checked : Qt::Unchecked; @@ -287,12 +295,13 @@ void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool } } - -AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVector mods) - : QDialog(parent), ui(new Ui::AtlOptionalModDialog) { +AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, ATLauncher::PackVersion version, QVector mods) + : QDialog(parent) + , ui(new Ui::AtlOptionalModDialog) +{ ui->setupUi(this); - listModel = new AtlOptionalModListModel(this, mods); + listModel = new AtlOptionalModListModel(this, version, mods); ui->treeView->setModel(listModel); ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index 953b288e..8e02444e 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -56,7 +56,7 @@ public: DescriptionColumn, }; - AtlOptionalModListModel(QWidget *parent, QVector mods); + AtlOptionalModListModel(QWidget *parent, ATLauncher::PackVersion version, QVector mods); QVector getResult(); @@ -86,7 +86,9 @@ private: NetJob::Ptr m_jobPtr; QByteArray m_response; + ATLauncher::PackVersion m_version; QVector m_mods; + QMap m_selection; QMap m_index; QMap> m_dependants; @@ -96,7 +98,7 @@ class AtlOptionalModDialog : public QDialog { Q_OBJECT public: - AtlOptionalModDialog(QWidget *parent, QVector mods); + AtlOptionalModDialog(QWidget *parent, ATLauncher::PackVersion version, QVector mods); ~AtlOptionalModDialog() override; QVector getResult() { diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index df9b9207..03923ed9 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -169,8 +169,9 @@ void AtlPage::onVersionSelectionChanged(QString data) suggestCurrent(); } -QVector AtlPage::chooseOptionalMods(QVector mods) { - AtlOptionalModDialog optionalModDialog(this, mods); +QVector AtlPage::chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) +{ + AtlOptionalModDialog optionalModDialog(this, version, mods); optionalModDialog.exec(); return optionalModDialog.getResult(); } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index c95b0127..eac86b51 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -84,7 +84,7 @@ private: void suggestCurrent(); QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override; - QVector chooseOptionalMods(QVector mods) override; + QVector chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) override; private slots: void triggerSearch(); From 305973c0e7c07693a8b08d1908e64fc4986e13e0 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Thu, 5 May 2022 20:14:19 +0100 Subject: [PATCH 447/605] ATLauncher: Display install messages if applicable --- .../atlauncher/ATLPackInstallTask.cpp | 7 ++- .../atlauncher/ATLPackInstallTask.h | 45 +++++++++++++---- .../atlauncher/ATLPackManifest.cpp | 50 +++++++++++++++---- .../modplatform/atlauncher/ATLPackManifest.h | 46 +++++++++++++---- .../pages/modplatform/atlauncher/AtlPage.cpp | 13 ++++- .../ui/pages/modplatform/atlauncher/AtlPage.h | 1 + 6 files changed, 126 insertions(+), 36 deletions(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 90dc1365..9b14f355 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -95,14 +95,13 @@ void PackInstallTask::onDownloadSucceeded() qDebug() << "PackInstallTask::onDownloadSucceeded: " << QThread::currentThreadId(); jobPtr.reset(); - QJsonParseError parse_error; + QJsonParseError parse_error {}; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } - auto obj = doc.object(); ATLauncher::PackVersion version; @@ -117,6 +116,10 @@ void PackInstallTask::onDownloadSucceeded() } m_version = version; + // Display install message if one exists + if (!m_version.messages.install.isEmpty()) + m_support->displayMessage(m_version.messages.install); + auto ver = getComponentVersion("net.minecraft", m_version.minecraft); if (!ver) { emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft", m_version.minecraft)); diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index 6bc30689..f0af4e3a 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield - * Copyright 2021 Petr Mrazek + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -45,6 +64,10 @@ public: */ virtual QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) = 0; + /** + * Requests a user interaction to display a message. + */ + virtual void displayMessage(QString message) = 0; }; class PackInstallTask : public InstanceTask diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp index a8f2711b..259c170c 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp +++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield - * Copyright 2021 Petr Mrazek + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "ATLPackManifest.h" @@ -186,6 +205,12 @@ static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { p.effectively_hidden = p.hidden || p.library; } +static void loadVersionMessages(ATLauncher::VersionMessages& m, QJsonObject& obj) +{ + m.install = Json::ensureString(obj, "install", ""); + m.update = Json::ensureString(obj, "update", ""); +} + void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) { v.version = Json::requireString(obj, "version"); @@ -238,4 +263,7 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) for (const auto &key : colourObj.keys()) { v.colours[key] = Json::requireString(colourObj.value(key), "colour"); } + + auto messages = Json::ensureObject(obj, "messages"); + loadVersionMessages(v.messages, messages); } diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h index 2911107e..931a11dc 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -1,17 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020 Jamie Mansfield + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020 Jamie Mansfield + * + * 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 @@ -124,6 +143,12 @@ struct VersionConfigs QString sha1; }; +struct VersionMessages +{ + QString install; + QString update; +}; + struct PackVersion { QString version; @@ -138,6 +163,7 @@ struct PackVersion VersionConfigs configs; QMap colours; + VersionMessages messages; }; void loadVersion(PackVersion & v, QJsonObject & obj); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index 03923ed9..7bc6fc6b 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -45,8 +45,12 @@ #include -AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent) - : QWidget(parent), ui(new Ui::AtlPage), dialog(dialog) +#include + +AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget* parent) + : QWidget(parent) + , ui(new Ui::AtlPage) + , dialog(dialog) { ui->setupUi(this); @@ -211,3 +215,8 @@ QString AtlPage::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVers vselect.exec(); return vselect.selectedVersion()->descriptor(); } + +void AtlPage::displayMessage(QString message) +{ + QMessageBox::information(this, tr("Installing"), message); +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index eac86b51..aa6d5da1 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -85,6 +85,7 @@ private: QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override; QVector chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) override; + void displayMessage(QString message) override; private slots: void triggerSearch(); From b84d52be3d1109efc2c9e35304831314050bd398 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Thu, 5 May 2022 20:58:12 +0100 Subject: [PATCH 448/605] ATLauncher: Display warnings when selecting optional mods --- .../modplatform/atlauncher/ATLPackManifest.cpp | 6 ++++++ .../modplatform/atlauncher/ATLPackManifest.h | 2 ++ .../atlauncher/AtlOptionalModDialog.cpp | 16 +++++++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp index 259c170c..d01ec32c 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp +++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -198,6 +198,7 @@ static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { } } p.colour = Json::ensureString(obj, QString("colour"), ""); + p.warning = Json::ensureString(obj, QString("warning"), ""); p.client = Json::ensureBoolean(obj, QString("client"), false); @@ -264,6 +265,11 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) v.colours[key] = Json::requireString(colourObj.value(key), "colour"); } + auto warningsObj = Json::ensureObject(obj, "warnings"); + for (const auto &key : warningsObj.keys()) { + v.warnings[key] = Json::requireString(warningsObj.value(key), "warning"); + } + auto messages = Json::ensureObject(obj, "messages"); loadVersionMessages(v.messages, messages); } diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h index 931a11dc..23e162e3 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -130,6 +130,7 @@ struct VersionMod QString group; QVector depends; QString colour; + QString warning; bool client; @@ -163,6 +164,7 @@ struct PackVersion VersionConfigs configs; QMap colours; + QMap warnings; VersionMessages messages; }; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index aee5a78e..004fdc57 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -231,7 +231,21 @@ void AtlOptionalModListModel::clearAll() { } void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) { - setMod(mod, index, !m_selection[mod.name]); + auto enable = !m_selection[mod.name]; + + // If there is a warning for the mod, display that first (if we would be enabling the mod) + if (enable && !mod.warning.isEmpty() && m_version.warnings.contains(mod.warning)) { + auto message = QString("%1

%2") + .arg(m_version.warnings[mod.warning], tr("Are you sure that you want to enable this mod?")); + + // fixme: avoid casting here + auto result = QMessageBox::warning((QWidget*) this->parent(), tr("Warning"), message, QMessageBox::Yes | QMessageBox::No); + if (result != QMessageBox::Yes) { + return; + } + } + + setMod(mod, index, enable); } void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) { From b2a89ee4b99f1d89dddee2918195f73b6b92c9db Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sat, 21 May 2022 16:59:01 +0200 Subject: [PATCH 449/605] change cf icon to a more fancy one taken from QuiltMC/art in the emoji folder, so it's licensed under CC0 --- .../multimc/128x128/instances/flame.png | Bin 3375 -> 6226 bytes .../multimc/32x32/instances/flame.png | Bin 849 -> 0 bytes launcher/resources/multimc/multimc.qrc | 4 +--- 3 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 launcher/resources/multimc/32x32/instances/flame.png diff --git a/launcher/resources/multimc/128x128/instances/flame.png b/launcher/resources/multimc/128x128/instances/flame.png index 8a50a0b418e8dc27ebb91ab3886d8bea40987104..6482975c494e02522f34a10894bb434f1ccc7194 100644 GIT binary patch literal 6226 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 YzkS2@~ literal 3375 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 b(Z!BG!B5Qc$A}(=00000NkvXXu0mjfi>7~8 diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index e22fe7ee..2337acd6 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -6,8 +6,7 @@ scalable/reddit-alien.svg - - 32x32/instances/flame.png + 128x128/instances/flame.png @@ -272,7 +271,6 @@ 32x32/instances/ftb_logo.png 128x128/instances/ftb_logo.png - 32x32/instances/flame.png 128x128/instances/flame.png 32x32/instances/gear.png From 35f71f5793ee91a71e00464932ff95eb5e5e4d5e Mon Sep 17 00:00:00 2001 From: Lenny McLennington Date: Wed, 11 May 2022 21:44:06 +0100 Subject: [PATCH 450/605] Support paste.gg, hastebin, and mclo.gs --- launcher/Application.cpp | 33 +++++- launcher/net/PasteUpload.cpp | 165 ++++++++++++++++++++++++--- launcher/net/PasteUpload.h | 28 ++++- launcher/ui/GuiUtil.cpp | 5 +- launcher/ui/pages/global/APIPage.cpp | 51 ++++++++- launcher/ui/pages/global/APIPage.h | 1 + launcher/ui/pages/global/APIPage.ui | 64 +++-------- 7 files changed, 272 insertions(+), 75 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ce62c41a..b36fd89a 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -36,6 +36,7 @@ #include "Application.h" #include "BuildConfig.h" +#include "net/PasteUpload.h" #include "ui/MainWindow.h" #include "ui/InstanceWindow.h" @@ -671,8 +672,36 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("UpdateDialogGeometry", ""); - // pastebin URL - m_settings->registerSetting("PastebinURL", "https://0x0.st"); + // This code feels so stupid is there a less stupid way of doing this? + { + m_settings->registerSetting("PastebinURL", ""); + QString pastebinURL = m_settings->get("PastebinURL").toString(); + + // If PastebinURL hasn't been set before then use the new default: mclo.gs + if (pastebinURL == "") { + m_settings->registerSetting("PastebinType", PasteUpload::PasteType::Mclogs); + m_settings->registerSetting("PastebinCustomAPIBase", ""); + } + // Otherwise: use 0x0.st + else { + // The default custom endpoint would usually be "" (meaning there is no custom endpoint specified) + // But if the user had customised the paste URL then that should be carried over into the custom endpoint. + QString defaultCustomEndpoint = (pastebinURL == "https://0x0.st") ? "" : pastebinURL; + m_settings->registerSetting("PastebinType", PasteUpload::PasteType::NullPointer); + m_settings->registerSetting("PastebinCustomAPIBase", defaultCustomEndpoint); + + m_settings->reset("PastebinURL"); + } + + bool ok; + unsigned int pasteType = m_settings->get("PastebinType").toUInt(&ok); + // If PastebinType is invalid then reset the related settings. + if (!ok || !(PasteUpload::PasteType::First <= pasteType && pasteType <= PasteUpload::PasteType::Last)) + { + m_settings->reset("PastebinType"); + m_settings->reset("PastebinCustomAPIBase"); + } + } m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("QuitAfterGameStop", false); diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 3d106c92..d583216d 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -42,8 +42,22 @@ #include #include -PasteUpload::PasteUpload(QWidget *window, QString text, QString url) : m_window(window), m_uploadUrl(url), m_text(text.toUtf8()) +std::array PasteUpload::PasteTypes = { + {{"0x0", "https://0x0.st", ""}, + {"hastebin", "https://hastebin.com", "/documents"}, + {"paste (paste.gg)", "https://paste.gg", "/api/v1/pastes"}, + {"mclogs", "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()) { + 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() @@ -53,26 +67,73 @@ PasteUpload::~PasteUpload() void PasteUpload::executeTask() { QNetworkRequest request{QUrl(m_uploadUrl)}; + QNetworkReply *rep{}; + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); - QHttpMultiPart *multiPart = new QHttpMultiPart{QHttpMultiPart::FormDataType}; + switch (m_pasteType) { + case NullPointer: { + QHttpMultiPart *multiPart = + new QHttpMultiPart{QHttpMultiPart::FormDataType}; - QHttpPart filePart; - filePart.setBody(m_text); - filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); - filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"file\"; filename=\"log.txt\""); + QHttpPart filePart; + filePart.setBody(m_text); + filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); + filePart.setHeader(QNetworkRequest::ContentDispositionHeader, + "form-data; name=\"file\"; filename=\"log.txt\""); + multiPart->append(filePart); - multiPart->append(filePart); + rep = APPLICATION->network()->post(request, multiPart); + multiPart->setParent(rep); - QNetworkReply *rep = APPLICATION->network()->post(request, multiPart); - multiPart->setParent(rep); + break; + } + case Hastebin: { + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + rep = APPLICATION->network()->post(request, m_text); + break; + } + case Mclogs: { + QUrlQuery postData; + postData.addQueryItem("content", m_text); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + rep = APPLICATION->network()->post(request, postData.toString().toUtf8()); + break; + } + case PasteGG: { + QJsonObject obj; + QJsonDocument doc; + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - m_reply = std::shared_ptr(rep); - setStatus(tr("Uploading to %1").arg(m_uploadUrl)); + obj.insert("expires", QDateTime::currentDateTimeUtc().addDays(100).toString(Qt::DateFormat::ISODate)); + + QJsonArray files; + QJsonObject logFileInfo; + QJsonObject logFileContentInfo; + logFileContentInfo.insert("format", "text"); + logFileContentInfo.insert("value", QString::fromUtf8(m_text)); + logFileInfo.insert("name", "log.txt"); + logFileInfo.insert("content", logFileContentInfo); + files.append(logFileInfo); + + obj.insert("files", files); + + doc.setObject(obj); + rep = APPLICATION->network()->post(request, doc.toJson()); + break; + } + } connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); - connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); - connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); + connect(rep, &QNetworkReply::finished, this, &PasteUpload::downloadFinished); + // This function call would be a lot shorter if we were using the latest Qt + connect(rep, + static_cast(&QNetworkReply::error), + this, &PasteUpload::downloadError); + + m_reply = std::shared_ptr(rep); + + setStatus(tr("Uploading to %1").arg(m_uploadUrl)); } void PasteUpload::downloadError(QNetworkReply::NetworkError error) @@ -102,6 +163,82 @@ void PasteUpload::downloadFinished() return; } - m_pasteLink = QString::fromUtf8(data).trimmed(); + switch (m_pasteType) + { + case NullPointer: + m_pasteLink = QString::fromUtf8(data).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; + } + else + { + emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); + qCritical() << m_uploadUrl << " returned malformed response body: " << data; + return; + } + break; + } + case Mclogs: { + QJsonDocument jsonDoc{QJsonDocument::fromJson(data)}; + QJsonObject jsonObj{jsonDoc.object()}; + if (jsonObj.contains("success") && jsonObj["success"].isBool()) + { + bool success = jsonObj["success"].toBool(); + if (success) + { + m_pasteLink = jsonObj["url"].toString(); + } + else + { + QString error = jsonObj["error"].toString(); + emitFailed(tr("Error: %1 returned an error: %2").arg(m_uploadUrl, error)); + qCritical() << m_uploadUrl << " returned error: " << error; + qCritical() << "Response body: " << data; + return; + } + } + else + { + emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); + qCritical() << m_uploadUrl << " returned malformed response body: " << data; + return; + } + break; + } + case PasteGG: + QJsonDocument jsonDoc{QJsonDocument::fromJson(data)}; + QJsonObject jsonObj{jsonDoc.object()}; + if (jsonObj.contains("status") && jsonObj["status"].isString()) + { + QString status = jsonObj["status"].toString(); + if (status == "success") + { + m_pasteLink = m_baseUrl + "/p/anonymous/" + jsonObj["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)); + qCritical() << m_uploadUrl << " returned error: " << error; + qCritical() << "Error message: " << message; + qCritical() << "Response body: " << data; + return; + } + } + else + { + emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); + qCritical() << m_uploadUrl << " returned malformed response body: " << data; + return; + } + break; + } emitSucceeded(); } diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index ea3a06d3..e276234f 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -36,14 +36,38 @@ #include "tasks/Task.h" #include +#include #include #include +#include class PasteUpload : public Task { Q_OBJECT public: - PasteUpload(QWidget *window, QString text, QString url); + enum PasteType : unsigned int { + // 0x0.st + NullPointer, + // hastebin.com + Hastebin, + // paste.gg + PasteGG, + // mclo.gs + Mclogs, + // Helpful to get the range of valid values on the enum for input sanitisation: + First = NullPointer, + Last = Mclogs + }; + + struct PasteTypeInfo { + const QString name; + const QString defaultBase; + const QString endpointPath; + }; + + static std::array PasteTypes; + + PasteUpload(QWidget *window, QString text, QString url, PasteType pasteType); virtual ~PasteUpload(); QString pasteLink() @@ -56,7 +80,9 @@ protected: private: QWidget *m_window; QString m_pasteLink; + QString m_baseUrl; QString m_uploadUrl; + PasteType m_pasteType; QByteArray m_text; std::shared_ptr m_reply; public diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 9eb658e2..5e9d1eda 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -16,8 +16,9 @@ QString GuiUtil::uploadPaste(const QString &text, QWidget *parentWidget) { ProgressDialog dialog(parentWidget); - auto pasteUrlSetting = APPLICATION->settings()->get("PastebinURL").toString(); - std::unique_ptr paste(new PasteUpload(parentWidget, text, pasteUrlSetting)); + auto pasteTypeSetting = static_cast(APPLICATION->settings()->get("PastebinType").toUInt()); + auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); + std::unique_ptr paste(new PasteUpload(parentWidget, text, pasteCustomAPIBaseSetting, pasteTypeSetting)); dialog.execWithTask(paste.get()); if (!paste->wasSuccessful()) diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 8b806bcf..b2827a19 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -3,6 +3,7 @@ * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2022 Lenny McLennington * * 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 @@ -46,15 +47,34 @@ #include "settings/SettingsObject.h" #include "tools/BaseProfiler.h" #include "Application.h" +#include "net/PasteUpload.h" APIPage::APIPage(QWidget *parent) : QWidget(parent), ui(new Ui::APIPage) { + // this is here so you can reorder the entries in the combobox without messing stuff up + unsigned int comboBoxEntries[] = { + PasteUpload::PasteType::Mclogs, + PasteUpload::PasteType::NullPointer, + PasteUpload::PasteType::PasteGG, + PasteUpload::PasteType::Hastebin + }; + static QRegularExpression validUrlRegExp("https?://.+"); + ui->setupUi(this); - ui->urlChoices->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->urlChoices)); - ui->tabWidget->tabBar()->hide();\ + + for (auto pasteType : comboBoxEntries) { + ui->pasteTypeComboBox->addItem(PasteUpload::PasteTypes.at(pasteType).name, pasteType); + } + + connect(ui->pasteTypeComboBox, static_cast(&QComboBox::currentIndexChanged), this, &APIPage::updateBaseURLPlaceholder); + // This function needs to be called even when the ComboBox's index is still in its default state. + updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); + ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry)); + ui->tabWidget->tabBar()->hide(); + loadSettings(); } @@ -63,11 +83,28 @@ APIPage::~APIPage() delete ui; } +void APIPage::updateBaseURLPlaceholder(int index) +{ + ui->baseURLEntry->setPlaceholderText(PasteUpload::PasteTypes.at(ui->pasteTypeComboBox->itemData(index).toUInt()).defaultBase); +} + void APIPage::loadSettings() { auto s = APPLICATION->settings(); - QString pastebinURL = s->get("PastebinURL").toString(); - ui->urlChoices->setCurrentText(pastebinURL); + + unsigned int pasteType = s->get("PastebinType").toUInt(); + QString pastebinURL = s->get("PastebinCustomAPIBase").toString(); + + ui->baseURLEntry->setText(pastebinURL); + int pasteTypeIndex = ui->pasteTypeComboBox->findData(pasteType); + if (pasteTypeIndex == -1) + { + pasteTypeIndex = ui->pasteTypeComboBox->findData(PasteUpload::PasteType::Mclogs); + ui->baseURLEntry->clear(); + } + + ui->pasteTypeComboBox->setCurrentIndex(pasteTypeIndex); + QString msaClientID = s->get("MSAClientIDOverride").toString(); ui->msaClientID->setText(msaClientID); QString curseKey = s->get("CFKeyOverride").toString(); @@ -77,8 +114,10 @@ void APIPage::loadSettings() void APIPage::applySettings() { auto s = APPLICATION->settings(); - QString pastebinURL = ui->urlChoices->currentText(); - s->set("PastebinURL", pastebinURL); + + s->set("PastebinType", ui->pasteTypeComboBox->currentData().toUInt()); + s->set("PastebinCustomAPIBase", ui->baseURLEntry->text()); + QString msaClientID = ui->msaClientID->text(); s->set("MSAClientIDOverride", msaClientID); QString curseKey = ui->curseKey->text(); diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index 20356009..0bb84c89 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -73,6 +73,7 @@ public: void retranslate() override; private: + void updateBaseURLPlaceholder(int index); void loadSettings(); void applySettings(); diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index eaa44c88..d986c2e2 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -6,8 +6,8 @@ 0 0 - 603 - 530 + 512 + 538 @@ -36,59 +36,30 @@ - &Pastebin URL + Pastebin Service - - - Qt::Horizontal - - - - - - - - 10 - - + - <html><head/><body><p>Note: only input that starts with <span style=" font-weight:600;">http://</span> or <span style=" font-weight:600;">https://</span> will be accepted.</p></body></html> - - - false + Paste Service Type - - - true - - - QComboBox::NoInsert - - - - https://0x0.st - - - + - + - <html><head/><body><p>Here you can choose from a predefined list of paste services, or input the URL of a different paste service of your choice, provided it supports the same protocol as 0x0.st, that is POST a file parameter to the URL and return a link in the response body.</p></body></html> + Base URL - - Qt::RichText - - - true - - - true + + + + + + @@ -101,13 +72,6 @@ &Microsoft Authentication - - - - Qt::Horizontal - - - From caf6d027282392a58b935185d787c4c22a861409 Mon Sep 17 00:00:00 2001 From: Lenny McLennington Date: Fri, 13 May 2022 17:48:19 +0100 Subject: [PATCH 451/605] Change paste settings and add copyright headers - There's now a notice reminding people to change the base URL if they had a custom base URL and change the paste type (that was something I personally had problems with when I was testing, so a reminder was helpful for me). - Broke down some of the long lines on APIPage.cpp to be more readable. - Added copyright headers where they were missing. - Changed the paste service display names to the names they are more commonly known by. - Changed the default hastebin base URL to https://hst.sh due to the acquisition of https://hastebin.com by Toptal. --- launcher/Application.cpp | 5 ++-- launcher/net/PasteUpload.cpp | 10 +++++--- launcher/net/PasteUpload.h | 3 ++- launcher/ui/GuiUtil.cpp | 37 +++++++++++++++++++++++++++- launcher/ui/pages/global/APIPage.cpp | 37 +++++++++++++++++++++++----- launcher/ui/pages/global/APIPage.h | 4 +++ launcher/ui/pages/global/APIPage.ui | 13 ++++++++++ 7 files changed, 95 insertions(+), 14 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index b36fd89a..40c6e760 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 Lenny McLennington * * 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 @@ -672,7 +673,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("UpdateDialogGeometry", ""); - // This code feels so stupid is there a less stupid way of doing this? + // HACK: This code feels so stupid is there a less stupid way of doing this? { m_settings->registerSetting("PastebinURL", ""); QString pastebinURL = m_settings->get("PastebinURL").toString(); @@ -694,7 +695,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) } bool ok; - unsigned int pasteType = m_settings->get("PastebinType").toUInt(&ok); + int pasteType = m_settings->get("PastebinType").toInt(&ok); // If PastebinType is invalid then reset the related settings. if (!ok || !(PasteUpload::PasteType::First <= pasteType && pasteType <= PasteUpload::PasteType::Last)) { diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index d583216d..3855190a 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Lenny McLennington + * Copyright (C) 2022 Swirl * * 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 @@ -43,10 +45,10 @@ #include std::array PasteUpload::PasteTypes = { - {{"0x0", "https://0x0.st", ""}, - {"hastebin", "https://hastebin.com", "/documents"}, - {"paste (paste.gg)", "https://paste.gg", "/api/v1/pastes"}, - {"mclogs", "https://api.mclo.gs", "/1/log"}}}; + {{"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()) { diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index e276234f..eb315c2b 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Lenny McLennington * * 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 @@ -45,7 +46,7 @@ class PasteUpload : public Task { Q_OBJECT public: - enum PasteType : unsigned int { + enum PasteType : int { // 0x0.st NullPointer, // hastebin.com diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 5e9d1eda..320f1502 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Lenny McLennington + * + * 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 "GuiUtil.h" #include @@ -16,7 +51,7 @@ QString GuiUtil::uploadPaste(const QString &text, QWidget *parentWidget) { ProgressDialog dialog(parentWidget); - auto pasteTypeSetting = static_cast(APPLICATION->settings()->get("PastebinType").toUInt()); + auto pasteTypeSetting = static_cast(APPLICATION->settings()->get("PastebinType").toInt()); auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); std::unique_ptr paste(new PasteUpload(parentWidget, text, pasteCustomAPIBaseSetting, pasteTypeSetting)); diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index b2827a19..2841544f 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -53,8 +53,8 @@ APIPage::APIPage(QWidget *parent) : QWidget(parent), ui(new Ui::APIPage) { - // this is here so you can reorder the entries in the combobox without messing stuff up - unsigned int comboBoxEntries[] = { + // This is here so you can reorder the entries in the combobox without messing stuff up + int comboBoxEntries[] = { PasteUpload::PasteType::Mclogs, PasteUpload::PasteType::NullPointer, PasteUpload::PasteType::PasteGG, @@ -69,13 +69,18 @@ APIPage::APIPage(QWidget *parent) : ui->pasteTypeComboBox->addItem(PasteUpload::PasteTypes.at(pasteType).name, pasteType); } - connect(ui->pasteTypeComboBox, static_cast(&QComboBox::currentIndexChanged), this, &APIPage::updateBaseURLPlaceholder); + void (QComboBox::*currentIndexChangedSignal)(int) (&QComboBox::currentIndexChanged); + connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLPlaceholder); // This function needs to be called even when the ComboBox's index is still in its default state. updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry)); ui->tabWidget->tabBar()->hide(); loadSettings(); + + resetBaseURLNote(); + connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLNote); + connect(ui->baseURLEntry, &QLineEdit::textEdited, this, &APIPage::resetBaseURLNote); } APIPage::~APIPage() @@ -83,16 +88,36 @@ APIPage::~APIPage() delete ui; } +void APIPage::resetBaseURLNote() +{ + ui->baseURLNote->hide(); + baseURLPasteType = ui->pasteTypeComboBox->currentIndex(); +} + +void APIPage::updateBaseURLNote(int index) +{ + if (baseURLPasteType == index) + { + ui->baseURLNote->hide(); + } + else if (!ui->baseURLEntry->text().isEmpty()) + { + ui->baseURLNote->show(); + } +} + void APIPage::updateBaseURLPlaceholder(int index) { - ui->baseURLEntry->setPlaceholderText(PasteUpload::PasteTypes.at(ui->pasteTypeComboBox->itemData(index).toUInt()).defaultBase); + int pasteType = ui->pasteTypeComboBox->itemData(index).toInt(); + QString pasteDefaultURL = PasteUpload::PasteTypes.at(pasteType).defaultBase; + ui->baseURLEntry->setPlaceholderText(pasteDefaultURL); } void APIPage::loadSettings() { auto s = APPLICATION->settings(); - unsigned int pasteType = s->get("PastebinType").toUInt(); + int pasteType = s->get("PastebinType").toInt(); QString pastebinURL = s->get("PastebinCustomAPIBase").toString(); ui->baseURLEntry->setText(pastebinURL); @@ -115,7 +140,7 @@ void APIPage::applySettings() { auto s = APPLICATION->settings(); - s->set("PastebinType", ui->pasteTypeComboBox->currentData().toUInt()); + s->set("PastebinType", ui->pasteTypeComboBox->currentData().toInt()); s->set("PastebinCustomAPIBase", ui->baseURLEntry->text()); QString msaClientID = ui->msaClientID->text(); diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index 0bb84c89..17e62ae7 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -3,6 +3,7 @@ * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2022 Lenny McLennington * * 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 @@ -73,6 +74,9 @@ public: void retranslate() override; private: + int baseURLPasteType; + void resetBaseURLNote(); + void updateBaseURLNote(int index); void updateBaseURLPlaceholder(int index); void loadSettings(); void applySettings(); diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index d986c2e2..b6af1958 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -61,6 +61,19 @@ + + true + + + + + + + Note: you probably want to change or clear the Base URL after changing the paste service type. + + + true + From e2ad3b01837e52a55e859412474978fa8a1e9625 Mon Sep 17 00:00:00 2001 From: Lenny McLennington Date: Tue, 17 May 2022 05:00:06 +0100 Subject: [PATCH 452/605] Add migration wizard, fix migration from custom paste instance - Very basic wizard just to allow the user to choose whether to keep their old paste settings or use the new default settings. - People who used custom 0x0 instances would just be kept on those settings and won't see the wizard. --- launcher/Application.cpp | 31 ++++---- launcher/CMakeLists.txt | 3 + launcher/ui/setupwizard/PasteWizardPage.cpp | 42 +++++++++++ launcher/ui/setupwizard/PasteWizardPage.h | 27 +++++++ launcher/ui/setupwizard/PasteWizardPage.ui | 80 +++++++++++++++++++++ 5 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 launcher/ui/setupwizard/PasteWizardPage.cpp create mode 100644 launcher/ui/setupwizard/PasteWizardPage.h create mode 100644 launcher/ui/setupwizard/PasteWizardPage.ui diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 40c6e760..438c7d61 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -63,6 +63,7 @@ #include "ui/setupwizard/SetupWizard.h" #include "ui/setupwizard/LanguageWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h" +#include "ui/setupwizard/PasteWizardPage.h" #include "ui/dialogs/CustomMessageBox.h" @@ -676,21 +677,17 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // HACK: This code feels so stupid is there a less stupid way of doing this? { m_settings->registerSetting("PastebinURL", ""); + m_settings->registerSetting("PastebinType", PasteUpload::PasteType::Mclogs); + m_settings->registerSetting("PastebinCustomAPIBase", ""); + QString pastebinURL = m_settings->get("PastebinURL").toString(); - // If PastebinURL hasn't been set before then use the new default: mclo.gs - if (pastebinURL == "") { - m_settings->registerSetting("PastebinType", PasteUpload::PasteType::Mclogs); - m_settings->registerSetting("PastebinCustomAPIBase", ""); - } - // Otherwise: use 0x0.st - else { - // The default custom endpoint would usually be "" (meaning there is no custom endpoint specified) - // But if the user had customised the paste URL then that should be carried over into the custom endpoint. - QString defaultCustomEndpoint = (pastebinURL == "https://0x0.st") ? "" : pastebinURL; - m_settings->registerSetting("PastebinType", PasteUpload::PasteType::NullPointer); - m_settings->registerSetting("PastebinCustomAPIBase", defaultCustomEndpoint); - + bool userHadNoPastebin = pastebinURL == ""; + bool userHadDefaultPastebin = pastebinURL == "https://0x0.st"; + if (!(userHadNoPastebin || userHadDefaultPastebin)) + { + m_settings->set("PastebinType", PasteUpload::PasteType::NullPointer); + m_settings->set("PastebinCustomAPIBase", pastebinURL); m_settings->reset("PastebinURL"); } @@ -929,7 +926,8 @@ bool Application::createSetupWizard() return true; return false; }(); - bool wizardRequired = javaRequired || languageRequired; + bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; + bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired; if(wizardRequired) { @@ -943,6 +941,11 @@ bool Application::createSetupWizard() { m_setupWizard->addPage(new JavaWizardPage(m_setupWizard)); } + + if (pasteInterventionRequired) + { + m_setupWizard->addPage(new PasteWizardPage(m_setupWizard)); + } connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished); m_setupWizard->show(); return true; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 8e75be20..15534c71 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -661,6 +661,8 @@ SET(LAUNCHER_SOURCES ui/setupwizard/JavaWizardPage.h ui/setupwizard/LanguageWizardPage.cpp ui/setupwizard/LanguageWizardPage.h + ui/setupwizard/PasteWizardPage.cpp + ui/setupwizard/PasteWizardPage.h # GUI - themes ui/themes/FusionTheme.cpp @@ -890,6 +892,7 @@ SET(LAUNCHER_SOURCES ) qt5_wrap_ui(LAUNCHER_UI + ui/setupwizard/PasteWizardPage.ui ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui diff --git a/launcher/ui/setupwizard/PasteWizardPage.cpp b/launcher/ui/setupwizard/PasteWizardPage.cpp new file mode 100644 index 00000000..0f47da4b --- /dev/null +++ b/launcher/ui/setupwizard/PasteWizardPage.cpp @@ -0,0 +1,42 @@ +#include "PasteWizardPage.h" +#include "ui_PasteWizardPage.h" + +#include "Application.h" +#include "net/PasteUpload.h" + +PasteWizardPage::PasteWizardPage(QWidget *parent) : + BaseWizardPage(parent), + ui(new Ui::PasteWizardPage) +{ + ui->setupUi(this); +} + +PasteWizardPage::~PasteWizardPage() +{ + delete ui; +} + +void PasteWizardPage::initializePage() +{ +} + +bool PasteWizardPage::validatePage() +{ + auto s = APPLICATION->settings(); + QString prevPasteURL = s->get("PastebinURL").toString(); + s->reset("PastebinURL"); + if (ui->previousSettingsRadioButton->isChecked()) + { + bool usingDefaultBase = prevPasteURL == PasteUpload::PasteTypes.at(PasteUpload::PasteType::NullPointer).defaultBase; + s->set("PastebinType", PasteUpload::PasteType::NullPointer); + if (!usingDefaultBase) + s->set("PastebinCustomAPIBase", prevPasteURL); + } + + return true; +} + +void PasteWizardPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/setupwizard/PasteWizardPage.h b/launcher/ui/setupwizard/PasteWizardPage.h new file mode 100644 index 00000000..513a14cb --- /dev/null +++ b/launcher/ui/setupwizard/PasteWizardPage.h @@ -0,0 +1,27 @@ +#ifndef PASTEDEFAULTSCONFIRMATIONWIZARD_H +#define PASTEDEFAULTSCONFIRMATIONWIZARD_H + +#include +#include "BaseWizardPage.h" + +namespace Ui { +class PasteWizardPage; +} + +class PasteWizardPage : public BaseWizardPage +{ + Q_OBJECT + +public: + explicit PasteWizardPage(QWidget *parent = nullptr); + ~PasteWizardPage(); + + void initializePage() override; + bool validatePage() override; + void retranslate() override; + +private: + Ui::PasteWizardPage *ui; +}; + +#endif // PASTEDEFAULTSCONFIRMATIONWIZARD_H diff --git a/launcher/ui/setupwizard/PasteWizardPage.ui b/launcher/ui/setupwizard/PasteWizardPage.ui new file mode 100644 index 00000000..247d3a75 --- /dev/null +++ b/launcher/ui/setupwizard/PasteWizardPage.ui @@ -0,0 +1,80 @@ + + + PasteWizardPage + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + The default paste service has changed to mclo.gs, please choose what you want to do with your settings. + + + true + + + + + + + Qt::Horizontal + + + + + + + Use new default service + + + true + + + buttonGroup + + + + + + + Keep previous settings + + + false + + + buttonGroup + + + + + + + Qt::Vertical + + + + 20 + 156 + + + + + + + + + + + + From de02deac989cc5efc135dc3c817fe72cc2499eca Mon Sep 17 00:00:00 2001 From: LennyMcLennington Date: Fri, 20 May 2022 22:30:00 +0100 Subject: [PATCH 453/605] Make if statement condition more readable Co-authored-by: Sefa Eyeoglu --- launcher/Application.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 438c7d61..91f5ef9d 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -682,9 +682,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) QString pastebinURL = m_settings->get("PastebinURL").toString(); - bool userHadNoPastebin = pastebinURL == ""; bool userHadDefaultPastebin = pastebinURL == "https://0x0.st"; - if (!(userHadNoPastebin || userHadDefaultPastebin)) + if (!pastebinURL.isEmpty() && !userHadDefaultPastebin) { m_settings->set("PastebinType", PasteUpload::PasteType::NullPointer); m_settings->set("PastebinCustomAPIBase", pastebinURL); From bfffcb3910b7f8429da16ae503fc79722d32ded6 Mon Sep 17 00:00:00 2001 From: txtsd Date: Sun, 22 May 2022 13:42:33 +0530 Subject: [PATCH 454/605] fix(workflow): Avoid invoking ccache on Release builds --- .github/workflows/build.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0590b348..38868b39 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,6 +39,7 @@ jobs: INSTALL_PORTABLE_DIR: "install-portable" INSTALL_APPIMAGE_DIR: "install-appdir" BUILD_DIR: "build" + CCACHE_VAR: "" steps: ## @@ -80,6 +81,12 @@ jobs: ccache -p # Show config ccache -z # Zero stats + - name: Use ccache on Debug builds only + if: inputs.build_type == 'Debug' + shell: bash + run: | + echo "CCACHE_VAR=ccache" >> $GITHUB_ENV + - name: Retrieve ccache cache (Windows) if: runner.os == 'Windows' && inputs.build_type == 'Debug' uses: actions/cache@v3.0.2 @@ -128,18 +135,18 @@ jobs: - name: Configure CMake (macOS) if: runner.os == 'macOS' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DQt5_DIR=/usr/local/opt/qt@5 -DCMAKE_PREFIX_PATH=/usr/local/opt/qt@5 -DLauncher_BUILD_PLATFORM=macOS -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DQt5_DIR=/usr/local/opt/qt@5 -DCMAKE_PREFIX_PATH=/usr/local/opt/qt@5 -DLauncher_BUILD_PLATFORM=macOS -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -G Ninja - name: Configure CMake (Windows) if: runner.os == 'Windows' shell: msys2 {0} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -G Ninja - name: Configure CMake (Linux) if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -G Ninja ## # BUILD From 90007e2d9d4f63cfc9dc73888af34a17657b5102 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 16:03:21 +0200 Subject: [PATCH 455/605] fix: temporarily ignore stringop-overflow warning --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index e07d2aa6..e6d66b8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,10 @@ set(CMAKE_CXX_FLAGS " -Wall -pedantic -Werror -Wno-deprecated-declarations -D_GL if(UNIX AND APPLE) set(CMAKE_CXX_FLAGS " -stdlib=libc++ ${CMAKE_CXX_FLAGS}") endif() +# FIXME: GCC 12 complains about some random stuff in QuaZip. Need to fix this later +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(CMAKE_CXX_FLAGS "-Wno-error=stringop-overflow ${CMAKE_CXX_FLAGS}") +endif() set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror=return-type") # Fix build with Qt 5.13 From c988b4d213b4125d298c893637a2362a7f192fce Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sun, 22 May 2022 17:16:00 +0200 Subject: [PATCH 456/605] fix appimage not having imageformats fixes stuff like the iris icon --- .github/workflows/build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5b70256a..6cbd5c21 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -113,12 +113,15 @@ jobs: if: runner.os == 'Linux' && matrix.appimage == true run: | sudo add-apt-repository ppa:savoury1/qt-5-15 + sudo add-apt-repository ppa:savoury1/kde-5-80 + sudo add-apt-repository ppa:savoury1/gpg + sudo add-apt-repository ppa:savoury1/ffmpeg4 - name: Install Qt (Linux) if: runner.os == 'Linux' run: | sudo apt-get -y update - sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 ninja-build + sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 ninja-build qt5-image-formats-plugins - name: Prepare AppImage (Linux) if: runner.os == 'Linux' && matrix.appimage == true From 0922a7f410d8675778bcf4720438efaa128b662b Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 20:50:37 +0200 Subject: [PATCH 457/605] refactor: use -O2 for release and -O1 for debug builds --- CMakeLists.txt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e6d66b8d..f54dd7ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,21 +34,24 @@ set(CMAKE_C_STANDARD_REQUIRED true) set(CMAKE_CXX_STANDARD 11) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) -set(CMAKE_CXX_FLAGS " -Wall -pedantic -Werror -Wno-deprecated-declarations -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS "-Wall -pedantic -Werror -Wno-deprecated-declarations -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}") if(UNIX AND APPLE) - set(CMAKE_CXX_FLAGS " -stdlib=libc++ ${CMAKE_CXX_FLAGS}") + set(CMAKE_CXX_FLAGS "-stdlib=libc++ ${CMAKE_CXX_FLAGS}") endif() -# FIXME: GCC 12 complains about some random stuff in QuaZip. Need to fix this later +# FIXME: GCC 12 complains about some random stuff in bundled QuaZip. Need to fix this later if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set(CMAKE_CXX_FLAGS "-Wno-error=stringop-overflow ${CMAKE_CXX_FLAGS}") endif() -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror=return-type") # Fix build with Qt 5.13 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00") +# set CXXFLAGS for build targets +set(CMAKE_CXX_FLAGS_DEBUG "-O1 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") + option(ENABLE_LTO "Enable Link Time Optimization" off) if(ENABLE_LTO) From 309dcc82cade6aee1af04534c8e307b56fcac848 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 20:57:52 +0200 Subject: [PATCH 458/605] Revert "fix: temporarily ignore stringop-overflow warning" This reverts commit 90007e2d9d4f63cfc9dc73888af34a17657b5102. --- CMakeLists.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f54dd7ba..ef4adf90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,10 +38,6 @@ set(CMAKE_CXX_FLAGS "-Wall -pedantic -Werror -Wno-deprecated-declarations -D_GLI if(UNIX AND APPLE) set(CMAKE_CXX_FLAGS "-stdlib=libc++ ${CMAKE_CXX_FLAGS}") endif() -# FIXME: GCC 12 complains about some random stuff in bundled QuaZip. Need to fix this later -if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - set(CMAKE_CXX_FLAGS "-Wno-error=stringop-overflow ${CMAKE_CXX_FLAGS}") -endif() # Fix build with Qt 5.13 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") From f00dbdc215c2de3b6906d8182388c27bbc657e24 Mon Sep 17 00:00:00 2001 From: dada513 Date: Wed, 13 Apr 2022 23:00:32 +0200 Subject: [PATCH 459/605] Make Metaserver changable in settings Co-authored-by: Sefa Eyeoglu Co-authored-by: flow --- launcher/Application.cpp | 2 + launcher/meta/BaseEntity.cpp | 11 +++- launcher/ui/pages/global/APIPage.cpp | 10 ++++ launcher/ui/pages/global/APIPage.ui | 75 +++++++++++++++++++++++++--- 4 files changed, 89 insertions(+), 9 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 91f5ef9d..ba4096b6 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -699,6 +699,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->reset("PastebinCustomAPIBase"); } } + // meta URL + m_settings->registerSetting("MetaURLOverride", ""); m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("QuitAfterGameStop", false); diff --git a/launcher/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp index 84155922..de4e1012 100644 --- a/launcher/meta/BaseEntity.cpp +++ b/launcher/meta/BaseEntity.cpp @@ -75,7 +75,16 @@ Meta::BaseEntity::~BaseEntity() QUrl Meta::BaseEntity::url() const { - return QUrl(BuildConfig.META_URL).resolved(localFilename()); + auto s = APPLICATION->settings(); + QString metaOverride = s->get("MetaURLOverride").toString(); + if(metaOverride.isEmpty()) + { + return QUrl(BuildConfig.META_URL).resolved(localFilename()); + } + else + { + return QUrl(metaOverride).resolved(localFilename()); + } } bool Meta::BaseEntity::loadLocalFile() diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 2841544f..af58b8cd 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -132,6 +132,8 @@ void APIPage::loadSettings() QString msaClientID = s->get("MSAClientIDOverride").toString(); ui->msaClientID->setText(msaClientID); + QString metaURL = s->get("MetaURLOverride").toString(); + ui->metaURL->setText(metaURL); QString curseKey = s->get("CFKeyOverride").toString(); ui->curseKey->setText(curseKey); } @@ -145,6 +147,14 @@ void APIPage::applySettings() QString msaClientID = ui->msaClientID->text(); s->set("MSAClientIDOverride", msaClientID); + QUrl metaURL = ui->metaURL->text(); + // Don't allow HTTP, since meta is basically RCE with all the jar files. + if(!metaURL.isEmpty() && metaURL.scheme() == "http") + { + metaURL.setScheme("https"); + } + + s->set("MetaURLOverride", metaURL); QString curseKey = ui->curseKey->text(); s->set("CFKeyOverride", curseKey); } diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index b6af1958..8d80df65 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -6,8 +6,8 @@ 0 0 - 512 - 538 + 800 + 600 @@ -85,6 +85,13 @@ &Microsoft Authentication + + + + Qt::Horizontal + + + @@ -125,12 +132,9 @@ - - - true - + - &CurseForge Core API + Meta&data Server @@ -140,8 +144,63 @@ + + + + You can set this to a third-party metadata server to use patched libraries or other hacks. + + + Qt::RichText + + + true + + + + + + + (Default) + + + + + Enter a custom URL for meta here. + + + Qt::RichText + + + true + + + true + + + + + + + + + + true + + + &CurseForge Core API + + + + + + Qt::Horizontal + + + + + Note: you probably don't need to set this if CurseForge already works. @@ -158,7 +217,7 @@ - + Enter a custom API Key for CurseForge here. From b181f4bc30f36778f9680eb54e6f3514739161e8 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 13:41:44 +0200 Subject: [PATCH 460/605] fix: improve spacing in APIPage --- launcher/ui/pages/global/APIPage.ui | 34 +++++++++++------------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 8d80df65..24189c5c 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -85,13 +85,6 @@ &Microsoft Authentication - - - - Qt::Horizontal - - - @@ -137,13 +130,6 @@ Meta&data Server - - - - Qt::Horizontal - - - @@ -192,13 +178,6 @@ &CurseForge Core API - - - - Qt::Horizontal - - - @@ -235,6 +214,19 @@ + + + + Qt::Vertical + + + + 20 + 40 + + + + From f2e205313485e458e2f5186f743d527d28609c5e Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 13:55:19 +0200 Subject: [PATCH 461/605] feat: add trailing slash to meta URL if it is missing --- launcher/ui/pages/global/APIPage.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index af58b8cd..6ad243dd 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -148,6 +148,13 @@ void APIPage::applySettings() QString msaClientID = ui->msaClientID->text(); s->set("MSAClientIDOverride", msaClientID); QUrl metaURL = ui->metaURL->text(); + // Add required trailing slash + if (!metaURL.isEmpty() && !metaURL.path().endsWith('/')) + { + QString path = metaURL.path(); + path.append('/'); + metaURL.setPath(path); + } // Don't allow HTTP, since meta is basically RCE with all the jar files. if(!metaURL.isEmpty() && metaURL.scheme() == "http") { From 0b85051a2363f4fad29477e3a0ccd3fda18fee01 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 21:41:41 +0200 Subject: [PATCH 462/605] fix: more generous optimizations for debug builds --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ef4adf90..a8c28e99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,7 +45,7 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00") # set CXXFLAGS for build targets -set(CMAKE_CXX_FLAGS_DEBUG "-O1 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS_DEBUG "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") option(ENABLE_LTO "Enable Link Time Optimization" off) From cb69869836d2b4ed4b50a43694e95c4a801332f7 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 22:04:24 +0200 Subject: [PATCH 463/605] revert: remove CurseForge workaround We have been asked by CurseForge to remove this workaround as it violates their terms of service. This is just a partial revert, as the UI changes were otherwise unrelated. This reverts commit 92e8aaf36f72b7527322add169b253d0698939d0, reversing changes made to 88a93945d4c9a11bf53016133335d359b819585e. --- launcher/modplatform/flame/FileResolvingTask.cpp | 16 +--------------- launcher/modplatform/flame/FlameModIndex.cpp | 9 +-------- launcher/modplatform/flame/PackManifest.cpp | 15 +++++---------- 3 files changed, 7 insertions(+), 33 deletions(-) diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 0deb99c4..95924a68 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -31,21 +31,7 @@ void Flame::FileResolvingTask::netJobFinished() for (auto& bytes : results) { auto& out = m_toProcess.files[index]; try { - bool fail = (!out.parseFromBytes(bytes)); - if(fail){ - //failed :( probably disabled mod, try to add to the list - auto doc = Json::requireDocument(bytes); - if (!doc.isObject()) { - throw JSONValidationError(QString("data is not an object? that's not supposed to happen")); - } - auto obj = Json::ensureObject(doc.object(), "data"); - //FIXME : HACK, MAY NOT WORK FOR LONG - out.url = QUrl(QString("https://media.forgecdn.net/files/%1/%2/%3") - .arg(QString::number(QString::number(out.fileId).leftRef(4).toInt()) - ,QString::number(QString::number(out.fileId).rightRef(3).toInt()) - ,QUrl::toPercentEncoding(out.fileName)), QUrl::TolerantMode); - } - failed &= fail; + failed &= (!out.parseFromBytes(bytes)); } catch (const JSONValidationError& e) { qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:"; qCritical() << e.cause(); diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 9846b156..ba0824cf 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -56,15 +56,8 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, file.fileId = Json::requireInteger(obj, "id"); file.date = Json::requireString(obj, "fileDate"); file.version = Json::requireString(obj, "displayName"); + file.downloadUrl = Json::requireString(obj, "downloadUrl"); file.fileName = Json::requireString(obj, "fileName"); - file.downloadUrl = Json::ensureString(obj, "downloadUrl", ""); - if(file.downloadUrl.isEmpty()){ - //FIXME : HACK, MAY NOT WORK FOR LONG - file.downloadUrl = QString("https://media.forgecdn.net/files/%1/%2/%3") - .arg(QString::number(QString::number(file.fileId.toInt()).leftRef(4).toInt()) - ,QString::number(QString::number(file.fileId.toInt()).rightRef(3).toInt()) - ,QUrl::toPercentEncoding(file.fileName)); - } unsortedVersions.append(file); } diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index 3217a756..e4f90c1a 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -71,6 +71,11 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes) fileName = Json::requireString(obj, "fileName"); + QString rawUrl = Json::requireString(obj, "downloadUrl"); + url = QUrl(rawUrl, QUrl::TolerantMode); + if (!url.isValid()) { + throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); + } // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience // It is also optional type = File::Type::SingleFile; @@ -82,17 +87,7 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes) // this is probably a mod, dunno what else could modpacks download targetFolder = "mods"; } - QString rawUrl = Json::ensureString(obj, "downloadUrl"); - if(rawUrl.isEmpty()){ - //either there somehow is an emtpy string as a link, or it's null either way it's invalid - //soft failing - return false; - } - url = QUrl(rawUrl, QUrl::TolerantMode); - if (!url.isValid()) { - throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); - } resolved = true; return true; } From d72c75db239dbff7e41c0d4a20df5337b9685a16 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 22:56:52 +0200 Subject: [PATCH 464/605] chore: bump version --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a8c28e99..e2635c3f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,8 +73,8 @@ set(Launcher_HELP_URL "https://polymc.org/wiki/help-pages/%1" CACHE STRING "URL ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 1) -set(Launcher_VERSION_MINOR 2) -set(Launcher_VERSION_HOTFIX 2) +set(Launcher_VERSION_MINOR 3) +set(Launcher_VERSION_HOTFIX 0) # Build number set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") From 2a0018e730a382a0d5dc963f30bf46cc0a240291 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Mon, 23 May 2022 08:29:30 +0800 Subject: [PATCH 465/605] add a .clang-format --- .clang-format | 211 ++++++++++++++++++++++++++++++++++++++++++++++++++ .gitignore | 1 - 2 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 .clang-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..ed96c030 --- /dev/null +++ b/.clang-format @@ -0,0 +1,211 @@ +--- +Language: Cpp +# BasedOnStyle: Chromium +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignArrayOfStructures: None +AlignConsecutiveMacros: false # changed +AlignConsecutiveAssignments: false # changed +AlignConsecutiveBitFields: None +AlignConsecutiveDeclarations: None +AlignEscapedNewlines: Left +AlignOperands: Align +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortEnumsOnASingleLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: false # changed +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +AttributeMacros: + - __capability +BinPackArguments: true +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: true # changed + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false # changed + SplitEmptyRecord: false # changed + SplitEmptyNamespace: false # changed +BreakBeforeBinaryOperators: None +BreakBeforeConceptDeclarations: true +BreakBeforeBraces: Custom # changed +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: true +BreakConstructorInitializers: BeforeComma # changed +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 140 # changed +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false # changed +DeriveLineEnding: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IfMacros: + - KJ_IF_MAYBE +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^<.*\.h>' + Priority: 1 + SortPriority: 0 + CaseSensitive: false + - Regex: '^<.*' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 3 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '([-_](test|unittest))?$' +IncludeIsMainSourceRegex: '' +IndentAccessModifiers: false +IndentCaseLabels: true +IndentCaseBlocks: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentExternBlock: AfterExternBlock +IndentRequires: false +IndentWidth: 4 # changed +IndentWrappedFunctionNames: false +InsertTrailingCommas: None +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +LambdaBodyIndentation: Signature +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PenaltyIndentedWhitespace: 0 +PointerAlignment: Left +PPIndentWidth: -1 +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + - ParseTestProto + - ParsePartialTestProto + CanonicalDelimiter: pb + BasedOnStyle: google +ReferenceAlignment: Pointer +ReflowComments: true +ShortNamespaceLines: 1 +SortIncludes: CaseSensitive +SortJavaStaticImport: Before +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceAroundPointerQualifiers: Default +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: Never +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +BitFieldColonSpacing: Both +Standard: Auto +StatementAttributeLikeMacros: + - Q_EMIT +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Never +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE + - NS_SWIFT_NAME + - CF_SWIFT_NAME +... + diff --git a/.gitignore b/.gitignore index 2a715656..f5917a46 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,6 @@ CMakeLists.txt.user.* /.settings /.idea /.vscode -.clang-format cmake-build-*/ Debug From 6d0ea13f97570f837f11022e3ef0fbfb6d0482f5 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Mon, 23 May 2022 16:50:17 +0800 Subject: [PATCH 466/605] make JVM args `PlainTextEdit` --- launcher/ui/pages/global/JavaPage.cpp | 6 +- launcher/ui/pages/global/JavaPage.ui | 99 +++++++++++++++------------ 2 files changed, 60 insertions(+), 45 deletions(-) diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index b5e8de6c..54bfb3cf 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -95,7 +95,7 @@ void JavaPage::applySettings() // Java Settings s->set("JavaPath", ui->javaPathTextBox->text()); - s->set("JvmArgs", ui->jvmArgsTextBox->text()); + s->set("JvmArgs", ui->jvmArgsTextBox->toPlainText()); s->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked()); s->set("IgnoreJavaWizard", ui->skipJavaWizardCheckbox->isChecked()); JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget()); @@ -120,7 +120,7 @@ void JavaPage::loadSettings() // Java Settings ui->javaPathTextBox->setText(s->get("JavaPath").toString()); - ui->jvmArgsTextBox->setText(s->get("JvmArgs").toString()); + ui->jvmArgsTextBox->setPlainText(s->get("JvmArgs").toString()); ui->skipCompatibilityCheckbox->setChecked(s->get("IgnoreJavaCompatibility").toBool()); ui->skipJavaWizardCheckbox->setChecked(s->get("IgnoreJavaWizard").toBool()); } @@ -166,7 +166,7 @@ void JavaPage::on_javaTestBtn_clicked() return; } checker.reset(new JavaCommon::TestCheck( - this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->text(), + this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->toPlainText(), ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value())); connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished())); checker->run(); diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index 3e4b12a1..6ccffed4 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -150,6 +150,35 @@ Java Runtime + + + + + 0 + 0 + + + + &Auto-detect... + + + + + + + + 0 + 0 + + + + JVM arguments: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + @@ -166,40 +195,8 @@ - - - - - 0 - 0 - - - - J&VM arguments: - - - jvmArgsTextBox - - - - - - - - 0 - 0 - - - - If enabled, the launcher will not check if an instance is compatible with the selected Java version. - - - &Skip Java compatibility checks - - - - - + + 0 @@ -207,7 +204,7 @@ - &Auto-detect... + &Test @@ -237,22 +234,22 @@ - - + + 0 0 + + If enabled, the launcher will not check if an instance is compatible with the selected Java version. + - &Test + &Skip Java compatibility checks - - - @@ -263,6 +260,25 @@ + + + + true + + + + 0 + 0 + + + + + 16777215 + 100 + + + + @@ -291,7 +307,6 @@ permGenSpinBox javaBrowseBtn javaPathTextBox - jvmArgsTextBox javaDetectBtn javaTestBtn tabWidget From c3e6b1aa8acdb7818bb678780f04ace0b68efad0 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Mon, 23 May 2022 18:40:46 +0800 Subject: [PATCH 467/605] use light bigsur icon --- program_info/org.polymc.PolyMC.bigsur.svg | 132 +++++++++++++++++----- program_info/polymc.icns | Bin 261369 -> 331581 bytes 2 files changed, 101 insertions(+), 31 deletions(-) diff --git a/program_info/org.polymc.PolyMC.bigsur.svg b/program_info/org.polymc.PolyMC.bigsur.svg index 1d680032..8297049b 100644 --- a/program_info/org.polymc.PolyMC.bigsur.svg +++ b/program_info/org.polymc.PolyMC.bigsur.svg @@ -1,32 +1,102 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/program_info/polymc.icns b/program_info/polymc.icns index a090c1b0d4be086a066f82a1f21514ccb974be0f..7365e919dec4f8241110641dedd7f849a9a8b95a 100644 GIT binary patch literal 331581 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%0D^ka4iDVgpbd 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%0D1kohoXW^QSi1w61HJ{5wh!SF-c}(^S_C#{ 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%0A3(XpKY!>F#GH@a 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 From b30b88716e67de93ea1c97d9dfd02a41af5428f3 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 13 Apr 2022 19:16:36 -0300 Subject: [PATCH 468/605] feat: add very early mod.toml packwiz support Also use it as a on-disk format for storing mod metadata. This will be used later on to make better mod managment. --- launcher/CMakeLists.txt | 8 +++ launcher/minecraft/mod/LocalModUpdateTask.cpp | 32 ++++++++++ launcher/minecraft/mod/LocalModUpdateTask.h | 26 ++++++++ launcher/modplatform/ModIndex.h | 31 ++++++++++ launcher/modplatform/flame/FlameModIndex.cpp | 1 + .../modrinth/ModrinthPackIndex.cpp | 1 + launcher/modplatform/packwiz/Packwiz.cpp | 60 +++++++++++++++++++ launcher/modplatform/packwiz/Packwiz.h | 43 +++++++++++++ 8 files changed, 202 insertions(+) create mode 100644 launcher/minecraft/mod/LocalModUpdateTask.cpp create mode 100644 launcher/minecraft/mod/LocalModUpdateTask.h create mode 100644 launcher/modplatform/packwiz/Packwiz.cpp create mode 100644 launcher/modplatform/packwiz/Packwiz.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 15534c71..b5c6fe91 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -331,6 +331,8 @@ set(MINECRAFT_SOURCES minecraft/mod/ModFolderLoadTask.cpp minecraft/mod/LocalModParseTask.h minecraft/mod/LocalModParseTask.cpp + minecraft/mod/LocalModUpdateTask.h + minecraft/mod/LocalModUpdateTask.cpp minecraft/mod/ResourcePackFolderModel.h minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/TexturePackFolderModel.h @@ -543,6 +545,11 @@ set(MODPACKSCH_SOURCES modplatform/modpacksch/FTBPackManifest.cpp ) +set(PACKWIZ_SOURCES + modplatform/packwiz/Packwiz.h + modplatform/packwiz/Packwiz.cpp +) + set(TECHNIC_SOURCES modplatform/technic/SingleZipPackInstallTask.h modplatform/technic/SingleZipPackInstallTask.cpp @@ -596,6 +603,7 @@ set(LOGIC_SOURCES ${FLAME_SOURCES} ${MODRINTH_SOURCES} ${MODPACKSCH_SOURCES} + ${PACKWIZ_SOURCES} ${TECHNIC_SOURCES} ${ATLAUNCHER_SOURCES} ) diff --git a/launcher/minecraft/mod/LocalModUpdateTask.cpp b/launcher/minecraft/mod/LocalModUpdateTask.cpp new file mode 100644 index 00000000..0f48217b --- /dev/null +++ b/launcher/minecraft/mod/LocalModUpdateTask.cpp @@ -0,0 +1,32 @@ +#include "LocalModUpdateTask.h" + +#include + +#include "FileSystem.h" +#include "modplatform/packwiz/Packwiz.h" + +LocalModUpdateTask::LocalModUpdateTask(QDir mods_dir, ModPlatform::IndexedPack& mod, ModPlatform::IndexedVersion& mod_version) + : m_mod(mod), m_mod_version(mod_version) +{ + // Ensure a '.index' folder exists in the mods folder, and create it if it does not + m_index_dir = { QString("%1/.index").arg(mods_dir.absolutePath()) }; + if (!FS::ensureFolderPathExists(m_index_dir.path())) { + emitFailed(QString("Unable to create index for mod %1!").arg(m_mod.name)); + } +} + +void LocalModUpdateTask::executeTask() +{ + setStatus(tr("Updating index for mod:\n%1").arg(m_mod.name)); + + auto pw_mod = Packwiz::createModFormat(m_index_dir, m_mod, m_mod_version); + Packwiz::updateModIndex(m_index_dir, pw_mod); + + emitSucceeded(); +} + +bool LocalModUpdateTask::abort() +{ + emitAborted(); + return true; +} diff --git a/launcher/minecraft/mod/LocalModUpdateTask.h b/launcher/minecraft/mod/LocalModUpdateTask.h new file mode 100644 index 00000000..866089e9 --- /dev/null +++ b/launcher/minecraft/mod/LocalModUpdateTask.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "tasks/Task.h" +#include "modplatform/ModIndex.h" + +class LocalModUpdateTask : public Task { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + explicit LocalModUpdateTask(QDir mods_dir, ModPlatform::IndexedPack& mod, ModPlatform::IndexedVersion& mod_version); + + bool canAbort() const override { return true; } + bool abort() override; + + protected slots: + //! Entry point for tasks. + void executeTask() override; + + private: + QDir m_index_dir; + ModPlatform::IndexedPack& m_mod; + ModPlatform::IndexedVersion& m_mod_version; +}; diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 7e1cf254..9c9ba99f 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -8,6 +8,35 @@ namespace ModPlatform { +enum class Provider{ + MODRINTH, + FLAME +}; + +class ProviderCapabilities { + public: + static QString hashType(Provider p) + { + switch(p){ + case Provider::MODRINTH: + return "sha256"; + case Provider::FLAME: + return "murmur2"; + } + return ""; + } + static QString providerName(Provider p) + { + switch(p){ + case Provider::MODRINTH: + return "modrinth"; + case Provider::FLAME: + return "curseforge"; + } + return ""; + } +}; + struct ModpackAuthor { QString name; QString url; @@ -26,6 +55,7 @@ struct IndexedVersion { struct IndexedPack { QVariant addonId; + Provider provider; QString name; QString description; QList authors; @@ -40,3 +70,4 @@ struct IndexedPack { } // namespace ModPlatform Q_DECLARE_METATYPE(ModPlatform::IndexedPack) +Q_DECLARE_METATYPE(ModPlatform::Provider) diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index ba0824cf..45f02b71 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -9,6 +9,7 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); + pack.provider = ModPlatform::Provider::FLAME; pack.name = Json::requireString(obj, "name"); pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.description = Json::ensureString(obj, "summary", ""); diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index f7fa9864..6c8659dc 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -28,6 +28,7 @@ static ModrinthAPI api; void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireString(obj, "project_id"); + pack.provider = ModPlatform::Provider::MODRINTH; pack.name = Json::requireString(obj, "title"); QString slug = Json::ensureString(obj, "slug", ""); diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp new file mode 100644 index 00000000..ff86a8a9 --- /dev/null +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -0,0 +1,60 @@ +#include "Packwiz.h" + +#include "modplatform/ModIndex.h" + +#include +#include +#include + +auto Packwiz::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod +{ + Mod mod; + + mod.name = mod_pack.name; + mod.filename = mod_version.fileName; + + mod.url = mod_version.downloadUrl; + mod.hash_format = ModPlatform::ProviderCapabilities::hashType(mod_pack.provider); + mod.hash = ""; // FIXME + + mod.provider = mod_pack.provider; + mod.file_id = mod_pack.addonId; + mod.project_id = mod_version.fileId; + + return mod; +} + +void Packwiz::updateModIndex(QDir& index_dir, Mod& mod) +{ + // Ensure the corresponding mod's info exists, and create it if not + auto index_file_name = QString("%1.toml").arg(mod.name); + QFile index_file(index_dir.absoluteFilePath(index_file_name)); + + // There's already data on there! + if (index_file.exists()) { index_file.remove(); } + + if (!index_file.open(QIODevice::ReadWrite)) { + qCritical() << "Could not open file " << index_file_name << "!"; + return; + } + + // Put TOML data into the file + QTextStream in_stream(&index_file); + auto addToStream = [&in_stream](QString&& key, QString value) { in_stream << QString("%1 = \"%2\"\n").arg(key, value); }; + + { + addToStream("name", mod.name); + addToStream("filename", mod.filename); + addToStream("side", mod.side); + + in_stream << QString("\n[download]\n"); + addToStream("url", mod.url.toString()); + addToStream("hash-format", mod.hash_format); + addToStream("hash", mod.hash); + + in_stream << QString("\n[update]\n"); + in_stream << QString("[update.%1]\n").arg(ModPlatform::ProviderCapabilities::providerName(mod.provider)); + addToStream("file-id", mod.file_id.toString()); + addToStream("project-id", mod.project_id.toString()); + } +} diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h new file mode 100644 index 00000000..64b95e7a --- /dev/null +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +namespace ModPlatform { +enum class Provider; +class IndexedPack; +class IndexedVersion; +} // namespace ModPlatform + +class QDir; + +class Packwiz { + public: + struct Mod { + QString name; + QString filename; + // FIXME: make side an enum + QString side = "both"; + + // [download] + QUrl url; + // FIXME: make hash-format an enum + QString hash_format; + QString hash; + + // [update] + ModPlatform::Provider provider; + QVariant file_id; + QVariant project_id; + }; + + /* Generates the object representing the information in a mod.toml file via its common representation in the launcher */ + static auto createModFormat(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); +}; From c86c719e1a09be2dc25ffd26278076566672e3b5 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 13 Apr 2022 19:18:28 -0300 Subject: [PATCH 469/605] feat: add mod index updating to ModDownloadTask This makes ModDownloadTask into a SequentialTask with 2 subtasks: Downloading the mod files and updating the index with the new information. The index updating is done first so that, in the future, we can prompt the user before download if, for instance, we discover there's another version already installed. --- launcher/ModDownloadTask.cpp | 25 +++++++++++----------- launcher/ModDownloadTask.h | 26 +++++++++++------------ launcher/ui/pages/modplatform/ModPage.cpp | 2 +- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp index 08a02d29..e5766435 100644 --- a/launcher/ModDownloadTask.cpp +++ b/launcher/ModDownloadTask.cpp @@ -1,24 +1,28 @@ #include "ModDownloadTask.h" #include "Application.h" +#include "minecraft/mod/LocalModUpdateTask.h" -ModDownloadTask::ModDownloadTask(const QUrl sourceUrl,const QString filename, const std::shared_ptr mods) -: m_sourceUrl(sourceUrl), mods(mods), filename(filename) { -} +ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr mods) + : m_mod(mod), m_mod_version(version), mods(mods) +{ + m_update_task.reset(new LocalModUpdateTask(mods->dir(), m_mod, m_mod_version)); -void ModDownloadTask::executeTask() { - setStatus(tr("Downloading mod:\n%1").arg(m_sourceUrl.toString())); + addTask(m_update_task); m_filesNetJob.reset(new NetJob(tr("Mod download"), APPLICATION->network())); - m_filesNetJob->addNetAction(Net::Download::makeFile(m_sourceUrl, mods->dir().absoluteFilePath(filename))); + m_filesNetJob->setStatus(tr("Downloading mod:\n%1").arg(m_mod_version.downloadUrl)); + + m_filesNetJob->addNetAction(Net::Download::makeFile(m_mod_version.downloadUrl, mods->dir().absoluteFilePath(getFilename()))); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ModDownloadTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &ModDownloadTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::failed, this, &ModDownloadTask::downloadFailed); - m_filesNetJob->start(); + + addTask(m_filesNetJob); + } void ModDownloadTask::downloadSucceeded() { - emitSucceeded(); m_filesNetJob.reset(); } @@ -32,8 +36,3 @@ void ModDownloadTask::downloadProgressChanged(qint64 current, qint64 total) { emit progress(current, total); } - -bool ModDownloadTask::abort() { - return m_filesNetJob->abort(); -} - diff --git a/launcher/ModDownloadTask.h b/launcher/ModDownloadTask.h index ddada5a2..d292dfbb 100644 --- a/launcher/ModDownloadTask.h +++ b/launcher/ModDownloadTask.h @@ -1,28 +1,26 @@ #pragma once + #include "QObjectPtr.h" -#include "tasks/Task.h" +#include "minecraft/mod/LocalModUpdateTask.h" +#include "modplatform/ModIndex.h" +#include "tasks/SequentialTask.h" #include "minecraft/mod/ModFolderModel.h" #include "net/NetJob.h" #include - -class ModDownloadTask : public Task { +class ModDownloadTask : public SequentialTask { Q_OBJECT public: - explicit ModDownloadTask(const QUrl sourceUrl, const QString filename, const std::shared_ptr mods); - const QString& getFilename() const { return filename; } - -public slots: - bool abort() override; -protected: - //! Entry point for tasks. - void executeTask() override; + explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr mods); + const QString& getFilename() const { return m_mod_version.fileName; } private: - QUrl m_sourceUrl; - NetJob::Ptr m_filesNetJob; + ModPlatform::IndexedPack m_mod; + ModPlatform::IndexedVersion m_mod_version; const std::shared_ptr mods; - const QString filename; + + NetJob::Ptr m_filesNetJob; + LocalModUpdateTask::Ptr m_update_task; void downloadProgressChanged(qint64 current, qint64 total); diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index ad36cf2f..5020d44c 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -150,7 +150,7 @@ void ModPage::onModSelected() if (dialog->isModSelected(current.name, version.fileName)) { dialog->removeSelectedMod(current.name); } else { - dialog->addSelectedMod(current.name, new ModDownloadTask(version.downloadUrl, version.fileName, dialog->mods)); + dialog->addSelectedMod(current.name, new ModDownloadTask(current, version, dialog->mods)); } updateSelectionButton(); From eaa5ce446765ef4305a1462d68e278b0797966ee Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 13 Apr 2022 19:23:12 -0300 Subject: [PATCH 470/605] feat(ui): adapt SequentialTask to nested SequentialTasks --- launcher/modplatform/packwiz/Packwiz.h | 5 ++--- launcher/tasks/SequentialTask.cpp | 12 +++++++++--- launcher/tasks/SequentialTask.h | 9 +++------ launcher/tasks/Task.h | 1 + 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 64b95e7a..9c90f7de 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -1,13 +1,12 @@ #pragma once +#include "modplatform/ModIndex.h" + #include #include #include namespace ModPlatform { -enum class Provider; -class IndexedPack; -class IndexedVersion; } // namespace ModPlatform class QDir; diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp index 1573e476..2d50c299 100644 --- a/launcher/tasks/SequentialTask.cpp +++ b/launcher/tasks/SequentialTask.cpp @@ -53,12 +53,18 @@ void SequentialTask::startNext() return; } Task::Ptr next = m_queue[m_currentIndex]; + connect(next.get(), SIGNAL(failed(QString)), this, SLOT(subTaskFailed(QString))); - connect(next.get(), SIGNAL(status(QString)), this, SLOT(subTaskStatus(QString))); - connect(next.get(), SIGNAL(progress(qint64, qint64)), this, SLOT(subTaskProgress(qint64, qint64))); connect(next.get(), SIGNAL(succeeded()), this, SLOT(startNext())); + connect(next.get(), SIGNAL(status(QString)), this, SLOT(subTaskStatus(QString))); + connect(next.get(), SIGNAL(stepStatus(QString)), this, SLOT(subTaskStatus(QString))); + + connect(next.get(), SIGNAL(progress(qint64, qint64)), this, SLOT(subTaskProgress(qint64, qint64))); + setStatus(tr("Executing task %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size())); + setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus()); + next->start(); } @@ -68,7 +74,7 @@ void SequentialTask::subTaskFailed(const QString& msg) } void SequentialTask::subTaskStatus(const QString& msg) { - setStepStatus(m_queue[m_currentIndex]->getStatus()); + setStepStatus(msg); } void SequentialTask::subTaskProgress(qint64 current, qint64 total) { diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h index 5b3c0111..e10cb6f7 100644 --- a/launcher/tasks/SequentialTask.h +++ b/launcher/tasks/SequentialTask.h @@ -32,13 +32,10 @@ slots: void subTaskStatus(const QString &msg); void subTaskProgress(qint64 current, qint64 total); -signals: - void stepStatus(QString status); +protected: + void setStepStatus(QString status) { m_step_status = status; emit stepStatus(status); }; -private: - void setStepStatus(QString status) { m_step_status = status; }; - -private: +protected: QString m_name; QString m_step_status; diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index f7765c3d..aafaf68c 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -92,6 +92,7 @@ class Task : public QObject { void aborted(); void failed(QString reason); void status(QString status); + void stepStatus(QString status); public slots: virtual void start(); From 8e4438b375ee904aa8225b569899355372e5987c Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 13 Apr 2022 21:25:08 -0300 Subject: [PATCH 471/605] feat: add parser for current impl of packwiz mod.toml This reads a local mod.toml file and extract information from it. Using C libs in C++ is kind of a pain tho :( --- launcher/modplatform/ModIndex.h | 2 +- launcher/modplatform/packwiz/Packwiz.cpp | 90 ++++++++++++++++++++++++ launcher/modplatform/packwiz/Packwiz.h | 5 ++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 9c9ba99f..c5329772 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -25,7 +25,7 @@ class ProviderCapabilities { } return ""; } - static QString providerName(Provider p) + static const char* providerName(Provider p) { switch(p){ case Provider::MODRINTH: diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index ff86a8a9..58bead82 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -1,6 +1,7 @@ #include "Packwiz.h" #include "modplatform/ModIndex.h" +#include "toml.h" #include #include @@ -58,3 +59,92 @@ void Packwiz::updateModIndex(QDir& index_dir, Mod& mod) addToStream("project-id", mod.project_id.toString()); } } + +auto Packwiz::getIndexForMod(QDir& index_dir, QString mod_name) -> Mod +{ + Mod mod; + + auto index_file_name = QString("%1.toml").arg(mod_name); + QFile index_file(index_dir.absoluteFilePath(index_file_name)); + + if (!index_file.exists()) { return mod; } + if (!index_file.open(QIODevice::ReadOnly)) { return mod; } + + toml_table_t* table; + + char errbuf[200]; + table = toml_parse(index_file.readAll().data(), errbuf, sizeof(errbuf)); + + index_file.close(); + + if (!table) { + qCritical() << QString("Could not open file %1").arg(index_file_name); + return mod; + } + + // Helper function for extracting data from the TOML file + auto stringEntry = [&](toml_table_t* parent, const char* entry_name) -> QString { + toml_datum_t var = toml_string_in(parent, entry_name); + if (!var.ok) { + qCritical() << QString("Failed to read property '%1' in mod metadata.").arg(entry_name); + return {}; + } + + QString tmp = var.u.s; + free(var.u.s); + + return tmp; + }; + + { // Basic info + mod.name = stringEntry(table, "name"); + // Basic sanity check + if (mod.name != mod_name) { + qCritical() << QString("Name mismatch in mod metadata:\nExpected:%1\nGot:%2").arg(mod_name, mod.name); + return {}; + } + + mod.filename = stringEntry(table, "filename"); + mod.side = stringEntry(table, "side"); + } + + { // [download] info + toml_table_t* download_table = toml_table_in(table, "download"); + if (!download_table) { + qCritical() << QString("No [download] section found on mod metadata!"); + return {}; + } + + mod.url = stringEntry(download_table, "url"); + mod.hash_format = stringEntry(download_table, "hash-format"); + mod.hash = stringEntry(download_table, "hash"); + } + + { // [update] info + using ProviderCaps = ModPlatform::ProviderCapabilities; + using Provider = ModPlatform::Provider; + + toml_table_t* update_table = toml_table_in(table, "update"); + if (!update_table) { + qCritical() << QString("No [update] section found on mod metadata!"); + return {}; + } + + toml_table_t* mod_provider_table; + if ((mod_provider_table = toml_table_in(update_table, ProviderCaps::providerName(Provider::FLAME)))) { + mod.provider = Provider::FLAME; + } else if ((mod_provider_table = toml_table_in(update_table, ProviderCaps::providerName(Provider::MODRINTH)))) { + mod.provider = Provider::MODRINTH; + } else { + qCritical() << "No mod provider on mod metadata!"; + return {}; + } + + mod.file_id = stringEntry(mod_provider_table, "file-id"); + mod.project_id = stringEntry(mod_provider_table, "project-id"); + } + + toml_free(table); + + return mod; +} diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 9c90f7de..08edaab9 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -39,4 +39,9 @@ class Packwiz { * 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); + + /* Gets the metadata for a mod with a particular name. + * If the mod doesn't have a metadata, it simply returns an empty Mod object. + * */ + static auto getIndexForMod(QDir& index_dir, QString mod_name) -> Mod; }; From e93b9560b5137a5ee7acdc34c0f74992aa02aad6 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 14 Apr 2022 22:02:41 -0300 Subject: [PATCH 472/605] feat: add method to delete mod metadata Also moves indexDir setting from LocalModUpdateTask -> ModFolderModel --- launcher/ModDownloadTask.cpp | 2 +- launcher/minecraft/mod/LocalModUpdateTask.cpp | 7 ++- launcher/minecraft/mod/ModFolderModel.h | 7 ++- launcher/modplatform/packwiz/Packwiz.cpp | 44 ++++++++++++++----- launcher/modplatform/packwiz/Packwiz.h | 5 ++- 5 files changed, 48 insertions(+), 17 deletions(-) diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp index e5766435..ad1e64e3 100644 --- a/launcher/ModDownloadTask.cpp +++ b/launcher/ModDownloadTask.cpp @@ -5,7 +5,7 @@ ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr mods) : m_mod(mod), m_mod_version(version), mods(mods) { - m_update_task.reset(new LocalModUpdateTask(mods->dir(), m_mod, m_mod_version)); + m_update_task.reset(new LocalModUpdateTask(mods->indexDir(), m_mod, m_mod_version)); addTask(m_update_task); diff --git a/launcher/minecraft/mod/LocalModUpdateTask.cpp b/launcher/minecraft/mod/LocalModUpdateTask.cpp index 0f48217b..63f5cf9a 100644 --- a/launcher/minecraft/mod/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/LocalModUpdateTask.cpp @@ -5,12 +5,11 @@ #include "FileSystem.h" #include "modplatform/packwiz/Packwiz.h" -LocalModUpdateTask::LocalModUpdateTask(QDir mods_dir, ModPlatform::IndexedPack& mod, ModPlatform::IndexedVersion& mod_version) - : m_mod(mod), m_mod_version(mod_version) +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) { // Ensure a '.index' folder exists in the mods folder, and create it if it does not - m_index_dir = { QString("%1/.index").arg(mods_dir.absolutePath()) }; - if (!FS::ensureFolderPathExists(m_index_dir.path())) { + if (!FS::ensureFolderPathExists(index_dir.path())) { emitFailed(QString("Unable to create index for mod %1!").arg(m_mod.name)); } } diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 62c504df..f8ad4ca8 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -108,11 +108,16 @@ public: bool isValid(); - QDir dir() + QDir& dir() { return m_dir; } + QDir indexDir() + { + return { QString("%1/.index").arg(dir().absolutePath()) }; + } + const QList & allMods() { return mods; diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 58bead82..bfadf7cb 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -7,6 +7,12 @@ #include #include +// Helpers +static inline QString indexFileName(QString const& mod_name) +{ + return QString("%1.toml").arg(mod_name); +} + auto Packwiz::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod { Mod mod; @@ -28,14 +34,13 @@ auto Packwiz::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pac void Packwiz::updateModIndex(QDir& index_dir, Mod& mod) { // Ensure the corresponding mod's info exists, and create it if not - auto index_file_name = QString("%1.toml").arg(mod.name); - QFile index_file(index_dir.absoluteFilePath(index_file_name)); + QFile index_file(index_dir.absoluteFilePath(indexFileName(mod.name))); // There's already data on there! if (index_file.exists()) { index_file.remove(); } if (!index_file.open(QIODevice::ReadWrite)) { - qCritical() << "Could not open file " << index_file_name << "!"; + qCritical() << QString("Could not open file %1!").arg(indexFileName(mod.name)); return; } @@ -60,15 +65,34 @@ void Packwiz::updateModIndex(QDir& index_dir, Mod& mod) } } -auto Packwiz::getIndexForMod(QDir& index_dir, QString mod_name) -> Mod +void Packwiz::deleteModIndex(QDir& index_dir, QString& mod_name) +{ + QFile index_file(index_dir.absoluteFilePath(indexFileName(mod_name))); + + if(!index_file.exists()){ + qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_name); + return; + } + + if(!index_file.remove()){ + qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_name); + } +} + +auto Packwiz::getIndexForMod(QDir& index_dir, QString& mod_name) -> Mod { Mod mod; - auto index_file_name = QString("%1.toml").arg(mod_name); - QFile index_file(index_dir.absoluteFilePath(index_file_name)); + QFile index_file(index_dir.absoluteFilePath(indexFileName(mod_name))); - if (!index_file.exists()) { return mod; } - if (!index_file.open(QIODevice::ReadOnly)) { return mod; } + if (!index_file.exists()) { + qWarning() << QString("Tried to get a non-existent mod metadata for %1").arg(mod_name); + return mod; + } + if (!index_file.open(QIODevice::ReadOnly)) { + qWarning() << QString("Failed to open mod metadata for %1").arg(mod_name); + return mod; + } toml_table_t* table; @@ -78,7 +102,7 @@ auto Packwiz::getIndexForMod(QDir& index_dir, QString mod_name) -> Mod index_file.close(); if (!table) { - qCritical() << QString("Could not open file %1").arg(index_file_name); + qCritical() << QString("Could not open file %1!").arg(indexFileName(mod.name)); return mod; } @@ -136,7 +160,7 @@ auto Packwiz::getIndexForMod(QDir& index_dir, QString mod_name) -> Mod } else if ((mod_provider_table = toml_table_in(update_table, ProviderCaps::providerName(Provider::MODRINTH)))) { mod.provider = Provider::MODRINTH; } else { - qCritical() << "No mod provider on mod metadata!"; + qCritical() << QString("No mod provider on mod metadata!"); return {}; } diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 08edaab9..541059d0 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -40,8 +40,11 @@ class Packwiz { * */ static void updateModIndex(QDir& index_dir, Mod& mod); + /* Deletes the metadata for the mod with the given name. If the metadata doesn't exist, it does nothing. */ + static void deleteModIndex(QDir& index_dir, QString& mod_name); + /* Gets the metadata for a mod with a particular name. * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ - static auto getIndexForMod(QDir& index_dir, QString mod_name) -> Mod; + static auto getIndexForMod(QDir& index_dir, QString& mod_name) -> Mod; }; From fcfb2cfc3da9a8f897063db05fdf3aebc41a59ae Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 15 Apr 2022 00:24:57 -0300 Subject: [PATCH 473/605] feat: use mod metadata for getting mod information For now this doesn't mean much, but it will help when we need data exclusive from the metadata, such as addon id and mod provider. Also removes the metadata when the mod is deleted, and make the Mod.h file a little more pleasing to look at :) --- launcher/minecraft/mod/Mod.cpp | 33 ++++++++- launcher/minecraft/mod/Mod.h | 73 +++++++------------- launcher/minecraft/mod/ModFolderLoadTask.cpp | 31 +++++++-- launcher/minecraft/mod/ModFolderLoadTask.h | 4 +- launcher/minecraft/mod/ModFolderModel.cpp | 9 ++- 5 files changed, 90 insertions(+), 60 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index b6bff29b..59f4d83b 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -33,6 +33,30 @@ Mod::Mod(const QFileInfo &file) m_changedDateTime = file.lastModified(); } +Mod::Mod(const QDir& mods_dir, const Packwiz::Mod& metadata) + : m_file(mods_dir.absoluteFilePath(metadata.filename)) + // It is weird, but name is not reliable for comparing with the JAR files name + // FIXME: Maybe use hash when implemented? + , m_mmc_id(metadata.filename) + , m_name(metadata.name) +{ + if(m_file.isDir()){ + m_type = MOD_FOLDER; + } + else{ + if (metadata.filename.endsWith(".zip") || metadata.filename.endsWith(".jar")) + m_type = MOD_ZIPFILE; + else if (metadata.filename.endsWith(".litemod")) + m_type = MOD_LITEMOD; + else + m_type = MOD_SINGLEFILE; + } + + m_from_metadata = true; + m_enabled = true; + m_changedDateTime = m_file.lastModified(); +} + void Mod::repath(const QFileInfo &file) { m_file = file; @@ -101,13 +125,18 @@ bool Mod::enable(bool value) if (!foo.rename(path)) return false; } - repath(QFileInfo(path)); + if(!fromMetadata()) + repath(QFileInfo(path)); + m_enabled = value; return true; } -bool Mod::destroy() +bool Mod::destroy(QDir& index_dir) { + // Delete metadata + Packwiz::deleteModIndex(index_dir, m_name); + m_type = MOD_UNKNOWN; return FS::deletePath(m_file.filePath()); } diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 921faeb1..c9fd5813 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -14,14 +14,14 @@ */ #pragma once -#include + #include +#include #include #include #include "ModDetails.h" - - +#include "modplatform/packwiz/Packwiz.h" class Mod { @@ -32,65 +32,41 @@ public: MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files. MOD_SINGLEFILE, //!< The mod is a single file (not a zip file). MOD_FOLDER, //!< The mod is in a folder on the filesystem. - MOD_LITEMOD, //!< The mod is a litemod + MOD_LITEMOD, //!< The mod is a litemod }; Mod() = default; Mod(const QFileInfo &file); + explicit Mod(const QDir& mods_dir, const Packwiz::Mod& metadata); - QFileInfo filename() const - { - return m_file; - } - QString mmc_id() const - { - return m_mmc_id; - } - ModType type() const - { - return m_type; - } - bool valid() - { - return m_type != MOD_UNKNOWN; - } + QFileInfo filename() const { return m_file; } + QDateTime dateTimeChanged() const { return m_changedDateTime; } + QString mmc_id() const { return m_mmc_id; } + ModType type() const { return m_type; } + bool fromMetadata() const { return m_from_metadata; } + bool enabled() const { return m_enabled; } - QDateTime dateTimeChanged() const - { - return m_changedDateTime; - } + bool valid() const { return m_type != MOD_UNKNOWN; } - bool enabled() const - { - return m_enabled; - } - - const ModDetails &details() const; - - QString name() const; - QString version() const; - QString homeurl() const; + const ModDetails& details() const; + QString name() const; + QString version() const; + QString homeurl() const; QString description() const; QStringList authors() const; bool enable(bool value); // delete all the files of this mod - bool destroy(); + bool destroy(QDir& index_dir); // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) void repath(const QFileInfo &file); - bool shouldResolve() { - return !m_resolving && !m_resolved; - } - bool isResolving() { - return m_resolving; - } - int resolutionTicket() - { - return m_resolutionTicket; - } + bool shouldResolve() const { return !m_resolving && !m_resolved; } + bool isResolving() const { return m_resolving; } + int resolutionTicket() const { return m_resolutionTicket; } + void setResolving(bool resolving, int resolutionTicket) { m_resolving = resolving; m_resolutionTicket = resolutionTicket; @@ -104,12 +80,15 @@ public: protected: QFileInfo m_file; QDateTime m_changedDateTime; + QString m_mmc_id; QString m_name; + ModType m_type = MOD_UNKNOWN; + bool m_from_metadata = false; + std::shared_ptr m_localDetails; + bool m_enabled = true; bool m_resolving = false; bool m_resolved = false; int m_resolutionTicket = 0; - ModType m_type = MOD_UNKNOWN; - std::shared_ptr m_localDetails; }; diff --git a/launcher/minecraft/mod/ModFolderLoadTask.cpp b/launcher/minecraft/mod/ModFolderLoadTask.cpp index 88349877..fd4d6008 100644 --- a/launcher/minecraft/mod/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/ModFolderLoadTask.cpp @@ -1,18 +1,35 @@ #include "ModFolderLoadTask.h" #include -ModFolderLoadTask::ModFolderLoadTask(QDir dir) : - m_dir(dir), m_result(new Result()) +#include "modplatform/packwiz/Packwiz.h" + +ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir) : + m_mods_dir(mods_dir), m_index_dir(index_dir), m_result(new Result()) { } void ModFolderLoadTask::run() { - m_dir.refresh(); - for (auto entry : m_dir.entryInfoList()) - { - Mod m(entry); - m_result->mods[m.mmc_id()] = m; + // Read metadata first + m_index_dir.refresh(); + for(auto entry : m_index_dir.entryList()){ + // QDir::Filter::NoDotAndDotDot seems to exclude all files for some reason... + if(entry == "." || entry == "..") + continue; + + entry.chop(5); // Remove .toml at the end + Mod mod(m_mods_dir, Packwiz::getIndexForMod(m_index_dir, entry)); + m_result->mods[mod.mmc_id()] = mod; } + + // Read JAR files that don't have metadata + m_mods_dir.refresh(); + for (auto entry : m_mods_dir.entryInfoList()) + { + Mod mod(entry); + if(!m_result->mods.contains(mod.mmc_id())) + m_result->mods[mod.mmc_id()] = mod; + } + emit succeeded(); } diff --git a/launcher/minecraft/mod/ModFolderLoadTask.h b/launcher/minecraft/mod/ModFolderLoadTask.h index 8d720e65..c869f083 100644 --- a/launcher/minecraft/mod/ModFolderLoadTask.h +++ b/launcher/minecraft/mod/ModFolderLoadTask.h @@ -19,11 +19,11 @@ public: } public: - ModFolderLoadTask(QDir dir); + ModFolderLoadTask(QDir& mods_dir, QDir& index_dir); void run(); signals: void succeeded(); private: - QDir m_dir; + QDir& m_mods_dir, m_index_dir; ResultPtr m_result; }; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index f0c53c39..615cfc0c 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -79,10 +79,14 @@ bool ModFolderModel::update() return true; } - auto task = new ModFolderLoadTask(m_dir); + auto index_dir = indexDir(); + auto task = new ModFolderLoadTask(dir(), index_dir); + m_update = task->result(); + QThreadPool *threadPool = QThreadPool::globalInstance(); connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate); + threadPool->start(task); return true; } @@ -334,7 +338,8 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes) for (auto i: indexes) { Mod &m = mods[i.row()]; - m.destroy(); + auto index_dir = indexDir(); + m.destroy(index_dir); } return true; } From 5a34e8fd7c913bc138e1606baf9df2cd1a64baed Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 15 Apr 2022 20:35:17 -0300 Subject: [PATCH 474/605] refactor: move mod tasks to their own subfolder Makes the launcher/minecraft/mod/ folder a little more organized. --- launcher/CMakeLists.txt | 12 ++++++------ launcher/ModDownloadTask.cpp | 3 ++- launcher/ModDownloadTask.h | 13 +++++++------ launcher/minecraft/mod/ModFolderModel.cpp | 14 ++++++++------ launcher/minecraft/mod/ModFolderModel.h | 4 ++-- .../mod/{ => tasks}/LocalModParseTask.cpp | 0 .../minecraft/mod/{ => tasks}/LocalModParseTask.h | 8 +++++--- .../mod/{ => tasks}/LocalModUpdateTask.cpp | 0 .../minecraft/mod/{ => tasks}/LocalModUpdateTask.h | 0 .../mod/{ => tasks}/ModFolderLoadTask.cpp | 0 .../minecraft/mod/{ => tasks}/ModFolderLoadTask.h | 7 ++++--- 11 files changed, 34 insertions(+), 27 deletions(-) rename launcher/minecraft/mod/{ => tasks}/LocalModParseTask.cpp (100%) rename launcher/minecraft/mod/{ => tasks}/LocalModParseTask.h (90%) rename launcher/minecraft/mod/{ => tasks}/LocalModUpdateTask.cpp (100%) rename launcher/minecraft/mod/{ => tasks}/LocalModUpdateTask.h (100%) rename launcher/minecraft/mod/{ => tasks}/ModFolderLoadTask.cpp (100%) rename launcher/minecraft/mod/{ => tasks}/ModFolderLoadTask.h (94%) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index b5c6fe91..b6df2851 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -327,16 +327,16 @@ set(MINECRAFT_SOURCES minecraft/mod/ModDetails.h minecraft/mod/ModFolderModel.h minecraft/mod/ModFolderModel.cpp - minecraft/mod/ModFolderLoadTask.h - minecraft/mod/ModFolderLoadTask.cpp - minecraft/mod/LocalModParseTask.h - minecraft/mod/LocalModParseTask.cpp - minecraft/mod/LocalModUpdateTask.h - minecraft/mod/LocalModUpdateTask.cpp minecraft/mod/ResourcePackFolderModel.h minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.cpp + minecraft/mod/tasks/ModFolderLoadTask.h + minecraft/mod/tasks/ModFolderLoadTask.cpp + minecraft/mod/tasks/LocalModParseTask.h + minecraft/mod/tasks/LocalModParseTask.cpp + minecraft/mod/tasks/LocalModUpdateTask.h + minecraft/mod/tasks/LocalModUpdateTask.cpp # Assets minecraft/AssetsUtils.h diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp index ad1e64e3..52de9c94 100644 --- a/launcher/ModDownloadTask.cpp +++ b/launcher/ModDownloadTask.cpp @@ -1,6 +1,7 @@ #include "ModDownloadTask.h" + #include "Application.h" -#include "minecraft/mod/LocalModUpdateTask.h" +#include "minecraft/mod/tasks/LocalModUpdateTask.h" ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr mods) : m_mod(mod), m_mod_version(version), mods(mods) diff --git a/launcher/ModDownloadTask.h b/launcher/ModDownloadTask.h index d292dfbb..5eaee187 100644 --- a/launcher/ModDownloadTask.h +++ b/launcher/ModDownloadTask.h @@ -1,12 +1,13 @@ #pragma once -#include "QObjectPtr.h" -#include "minecraft/mod/LocalModUpdateTask.h" -#include "modplatform/ModIndex.h" -#include "tasks/SequentialTask.h" -#include "minecraft/mod/ModFolderModel.h" -#include "net/NetJob.h" #include +#include "QObjectPtr.h" +#include "minecraft/mod/ModFolderModel.h" +#include "modplatform/ModIndex.h" +#include "net/NetJob.h" + +#include "tasks/SequentialTask.h" +#include "minecraft/mod/tasks/LocalModUpdateTask.h" class ModDownloadTask : public SequentialTask { Q_OBJECT diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 615cfc0c..936b68d3 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -14,17 +14,19 @@ */ #include "ModFolderModel.h" + #include +#include +#include #include +#include +#include #include #include -#include -#include -#include -#include "ModFolderLoadTask.h" -#include #include -#include "LocalModParseTask.h" + +#include "minecraft/mod/tasks/LocalModParseTask.h" +#include "minecraft/mod/tasks/ModFolderLoadTask.h" ModFolderModel::ModFolderModel(const QString &dir) : QAbstractListModel(), m_dir(dir) { diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index f8ad4ca8..10a72691 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -24,8 +24,8 @@ #include "Mod.h" -#include "ModFolderLoadTask.h" -#include "LocalModParseTask.h" +#include "minecraft/mod/tasks/ModFolderLoadTask.h" +#include "minecraft/mod/tasks/LocalModParseTask.h" class LegacyInstance; class BaseInstance; diff --git a/launcher/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp similarity index 100% rename from launcher/minecraft/mod/LocalModParseTask.cpp rename to launcher/minecraft/mod/tasks/LocalModParseTask.cpp diff --git a/launcher/minecraft/mod/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h similarity index 90% rename from launcher/minecraft/mod/LocalModParseTask.h rename to launcher/minecraft/mod/tasks/LocalModParseTask.h index 0f119ba6..ed92394c 100644 --- a/launcher/minecraft/mod/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -1,9 +1,11 @@ #pragma once -#include + #include #include -#include "Mod.h" -#include "ModDetails.h" +#include + +#include "minecraft/mod/Mod.h" +#include "minecraft/mod/ModDetails.h" class LocalModParseTask : public QObject, public QRunnable { diff --git a/launcher/minecraft/mod/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp similarity index 100% rename from launcher/minecraft/mod/LocalModUpdateTask.cpp rename to launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp diff --git a/launcher/minecraft/mod/LocalModUpdateTask.h b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h similarity index 100% rename from launcher/minecraft/mod/LocalModUpdateTask.h rename to launcher/minecraft/mod/tasks/LocalModUpdateTask.h diff --git a/launcher/minecraft/mod/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp similarity index 100% rename from launcher/minecraft/mod/ModFolderLoadTask.cpp rename to launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp diff --git a/launcher/minecraft/mod/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h similarity index 94% rename from launcher/minecraft/mod/ModFolderLoadTask.h rename to launcher/minecraft/mod/tasks/ModFolderLoadTask.h index c869f083..bb66022a 100644 --- a/launcher/minecraft/mod/ModFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h @@ -1,10 +1,11 @@ #pragma once -#include -#include + #include #include -#include "Mod.h" +#include +#include #include +#include "minecraft/mod/Mod.h" class ModFolderLoadTask : public QObject, public QRunnable { From e9fb566c0797865a37e5b59a49163258b3adb328 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 15 Apr 2022 22:07:35 -0300 Subject: [PATCH 475/605] refactor: remove unused mod info and organize some stuff --- launcher/minecraft/mod/Mod.cpp | 87 ++++++++----------- launcher/minecraft/mod/Mod.h | 4 +- launcher/minecraft/mod/ModDetails.h | 15 +++- launcher/minecraft/mod/ModFolderModel.cpp | 10 +-- .../minecraft/mod/tasks/LocalModParseTask.cpp | 22 +---- .../minecraft/mod/tasks/ModFolderLoadTask.cpp | 22 +++-- launcher/ui/widgets/MCModInfoFrame.cpp | 2 +- 7 files changed, 67 insertions(+), 95 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 59f4d83b..64c9ffb5 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -13,12 +13,13 @@ * limitations under the License. */ +#include "Mod.h" + #include #include -#include "Mod.h" -#include #include +#include namespace { @@ -26,8 +27,7 @@ ModDetails invalidDetails; } - -Mod::Mod(const QFileInfo &file) +Mod::Mod(const QFileInfo& file) { repath(file); m_changedDateTime = file.lastModified(); @@ -37,13 +37,12 @@ Mod::Mod(const QDir& mods_dir, const Packwiz::Mod& metadata) : m_file(mods_dir.absoluteFilePath(metadata.filename)) // It is weird, but name is not reliable for comparing with the JAR files name // FIXME: Maybe use hash when implemented? - , m_mmc_id(metadata.filename) + , m_internal_id(metadata.filename) , m_name(metadata.name) { - if(m_file.isDir()){ + if (m_file.isDir()) { m_type = MOD_FOLDER; - } - else{ + } else { if (metadata.filename.endsWith(".zip") || metadata.filename.endsWith(".jar")) m_type = MOD_ZIPFILE; else if (metadata.filename.endsWith(".litemod")) @@ -57,43 +56,32 @@ Mod::Mod(const QDir& mods_dir, const Packwiz::Mod& metadata) m_changedDateTime = m_file.lastModified(); } -void Mod::repath(const QFileInfo &file) +void Mod::repath(const QFileInfo& file) { m_file = file; QString name_base = file.fileName(); m_type = Mod::MOD_UNKNOWN; - m_mmc_id = name_base; + m_internal_id = name_base; - if (m_file.isDir()) - { + if (m_file.isDir()) { m_type = MOD_FOLDER; m_name = name_base; - } - else if (m_file.isFile()) - { - if (name_base.endsWith(".disabled")) - { + } else if (m_file.isFile()) { + if (name_base.endsWith(".disabled")) { m_enabled = false; name_base.chop(9); - } - else - { + } else { m_enabled = true; } - if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) - { + if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) { m_type = MOD_ZIPFILE; name_base.chop(4); - } - else if (name_base.endsWith(".litemod")) - { + } else if (name_base.endsWith(".litemod")) { m_type = MOD_LITEMOD; name_base.chop(8); - } - else - { + } else { m_type = MOD_SINGLEFILE; } m_name = name_base; @@ -109,23 +97,22 @@ bool Mod::enable(bool value) return false; QString path = m_file.absoluteFilePath(); - if (value) - { - QFile foo(path); + QFile file(path); + if (value) { if (!path.endsWith(".disabled")) return false; path.chop(9); - if (!foo.rename(path)) + + if (!file.rename(path)) return false; - } - else - { - QFile foo(path); + } else { path += ".disabled"; - if (!foo.rename(path)) + + if (!file.rename(path)) return false; } - if(!fromMetadata()) + + if (!fromMetadata()) repath(QFileInfo(path)); m_enabled = value; @@ -141,29 +128,25 @@ bool Mod::destroy(QDir& index_dir) return FS::deletePath(m_file.filePath()); } - -const ModDetails & Mod::details() const +const ModDetails& Mod::details() const { - if(!m_localDetails) - return invalidDetails; - return *m_localDetails; -} - - -QString Mod::version() const -{ - return details().version; + return m_localDetails ? *m_localDetails : invalidDetails; } QString Mod::name() const { - auto & d = details(); - if(!d.name.isEmpty()) { - return d.name; + auto d_name = details().name; + if (!d_name.isEmpty()) { + return d_name; } return m_name; } +QString Mod::version() const +{ + return details().version; +} + QString Mod::homeurl() const { return details().homeurl; diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index c9fd5813..46bb1a59 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -41,7 +41,7 @@ public: QFileInfo filename() const { return m_file; } QDateTime dateTimeChanged() const { return m_changedDateTime; } - QString mmc_id() const { return m_mmc_id; } + QString internal_id() const { return m_internal_id; } ModType type() const { return m_type; } bool fromMetadata() const { return m_from_metadata; } bool enabled() const { return m_enabled; } @@ -81,7 +81,7 @@ protected: QFileInfo m_file; QDateTime m_changedDateTime; - QString m_mmc_id; + QString m_internal_id; QString m_name; ModType m_type = MOD_UNKNOWN; bool m_from_metadata = false; diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index 6ab4aee7..d8d4f66f 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -5,13 +5,24 @@ struct ModDetails { + /* Mod ID as defined in the ModLoader-specific metadata */ QString mod_id; + + /* Human-readable name */ QString name; + + /* Human-readable mod version */ QString version; + + /* Human-readable minecraft version */ QString mcversion; + + /* URL for mod's home page */ QString homeurl; - QString updateurl; + + /* Human-readable description */ QString description; + + /* List of the author's names */ QStringList authors; - QString credits; }; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 936b68d3..e2e041eb 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -159,7 +159,7 @@ void ModFolderModel::finishUpdate() modsIndex.clear(); int idx = 0; for(auto & mod: mods) { - modsIndex[mod.mmc_id()] = idx; + modsIndex[mod.internal_id()] = idx; idx++; } } @@ -182,7 +182,7 @@ void ModFolderModel::resolveMod(Mod& m) auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.filename()); auto result = task->result(); - result->id = m.mmc_id(); + result->id = m.internal_id(); activeTickets.insert(nextResolutionTicket, result); m.setResolving(true, nextResolutionTicket); nextResolutionTicket++; @@ -388,7 +388,7 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const } case Qt::ToolTipRole: - return mods[row].mmc_id(); + return mods[row].internal_id(); case Qt::CheckStateRole: switch (column) @@ -443,11 +443,11 @@ bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction actio } // preserve the row, but change its ID - auto oldId = mod.mmc_id(); + auto oldId = mod.internal_id(); if(!mod.enable(!mod.enabled())) { return false; } - auto newId = mod.mmc_id(); + auto newId = mod.internal_id(); if(modsIndex.contains(newId)) { // NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled // But is it necessary? diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index a7bec5ae..3354732b 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -35,7 +35,6 @@ std::shared_ptr ReadMCModInfo(QByteArray contents) details->name = name; } details->version = firstObj.value("version").toString(); - details->updateurl = firstObj.value("updateUrl").toString(); auto homeurl = firstObj.value("url").toString().trimmed(); if(!homeurl.isEmpty()) { @@ -57,7 +56,6 @@ std::shared_ptr ReadMCModInfo(QByteArray contents) { details->authors.append(author.toString()); } - details->credits = firstObj.value("credits").toString(); return details; }; QJsonParseError jsonError; @@ -168,27 +166,9 @@ std::shared_ptr ReadMCModTOML(QByteArray contents) } if(!authors.isEmpty()) { - // author information is stored as a string now, not a list details->authors.append(authors); } - // is credits even used anywhere? including this for completion/parity with old data version - toml_datum_t creditsDatum = toml_string_in(tomlData, "credits"); - QString credits = ""; - if(creditsDatum.ok) - { - authors = creditsDatum.u.s; - free(creditsDatum.u.s); - } - else - { - creditsDatum = toml_string_in(tomlModsTable0, "credits"); - if(creditsDatum.ok) - { - credits = creditsDatum.u.s; - free(creditsDatum.u.s); - } - } - details->credits = credits; + toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL"); QString homeurl = ""; if(homeurlDatum.ok) diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index fd4d6008..bf7b28d6 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -3,32 +3,30 @@ #include "modplatform/packwiz/Packwiz.h" -ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir) : - m_mods_dir(mods_dir), m_index_dir(index_dir), m_result(new Result()) -{ -} +ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir) + : m_mods_dir(mods_dir), m_index_dir(index_dir), m_result(new Result()) +{} void ModFolderLoadTask::run() { // Read metadata first m_index_dir.refresh(); - for(auto entry : m_index_dir.entryList()){ + for (auto entry : m_index_dir.entryList()) { // QDir::Filter::NoDotAndDotDot seems to exclude all files for some reason... - if(entry == "." || entry == "..") + if (entry == "." || entry == "..") continue; - entry.chop(5); // Remove .toml at the end + entry.chop(5); // Remove .toml at the end Mod mod(m_mods_dir, Packwiz::getIndexForMod(m_index_dir, entry)); - m_result->mods[mod.mmc_id()] = mod; + m_result->mods[mod.internal_id()] = mod; } // Read JAR files that don't have metadata m_mods_dir.refresh(); - for (auto entry : m_mods_dir.entryInfoList()) - { + for (auto entry : m_mods_dir.entryInfoList()) { Mod mod(entry); - if(!m_result->mods.contains(mod.mmc_id())) - m_result->mods[mod.mmc_id()] = mod; + if (!m_result->mods.contains(mod.internal_id())) + m_result->mods[mod.internal_id()] = mod; } emit succeeded(); diff --git a/launcher/ui/widgets/MCModInfoFrame.cpp b/launcher/ui/widgets/MCModInfoFrame.cpp index 8c4bd690..7d78006b 100644 --- a/launcher/ui/widgets/MCModInfoFrame.cpp +++ b/launcher/ui/widgets/MCModInfoFrame.cpp @@ -32,7 +32,7 @@ void MCModInfoFrame::updateWithMod(Mod &m) QString text = ""; QString name = ""; if (m.name().isEmpty()) - name = m.mmc_id(); + name = m.internal_id(); else name = m.name(); From 092d2f8917271264871d69239ecb8836b34d0994 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 15 Apr 2022 22:37:10 -0300 Subject: [PATCH 476/605] feat: add support for converting builtin -> packwiz mod formats Also adds more documentation. --- launcher/modplatform/packwiz/Packwiz.cpp | 41 ++++++++++++++++++++---- launcher/modplatform/packwiz/Packwiz.h | 36 +++++++++++++-------- 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index bfadf7cb..445d64fb 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -1,12 +1,14 @@ #include "Packwiz.h" -#include "modplatform/ModIndex.h" -#include "toml.h" - #include #include #include +#include "toml.h" + +#include "modplatform/ModIndex.h" +#include "minecraft/mod/Mod.h" + // Helpers static inline QString indexFileName(QString const& mod_name) { @@ -31,12 +33,39 @@ auto Packwiz::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pac return mod; } +auto Packwiz::createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod +{ + auto mod_name = internal_mod.name(); + + // Try getting metadata if it exists + Mod mod { getIndexForMod(index_dir, mod_name) }; + if(mod.isValid()) + return mod; + + // Manually construct packwiz mod + mod.name = internal_mod.name(); + mod.filename = internal_mod.filename().fileName(); + + // TODO: Have a mechanism for telling the UI subsystem that we want to gather user information + // (i.e. which mod provider we want to use). Maybe an object parameter with a signal for that? + + return mod; +} + void Packwiz::updateModIndex(QDir& index_dir, Mod& mod) { + if(!mod.isValid()){ + qCritical() << QString("Tried to update metadata of an invalid mod!"); + return; + } + // Ensure the corresponding mod's info exists, and create it if not QFile index_file(index_dir.absoluteFilePath(indexFileName(mod.name))); // There's already data on there! + // TODO: We should do more stuff here, as the user is likely trying to + // override a file. In this case, check versions and ask the user what + // they want to do! if (index_file.exists()) { index_file.remove(); } if (!index_file.open(QIODevice::ReadWrite)) { @@ -87,11 +116,11 @@ auto Packwiz::getIndexForMod(QDir& index_dir, QString& mod_name) -> Mod if (!index_file.exists()) { qWarning() << QString("Tried to get a non-existent mod metadata for %1").arg(mod_name); - return mod; + return {}; } if (!index_file.open(QIODevice::ReadOnly)) { qWarning() << QString("Failed to open mod metadata for %1").arg(mod_name); - return mod; + return {}; } toml_table_t* table; @@ -103,7 +132,7 @@ auto Packwiz::getIndexForMod(QDir& index_dir, QString& mod_name) -> Mod if (!table) { qCritical() << QString("Could not open file %1!").arg(indexFileName(mod.name)); - return mod; + return {}; } // Helper function for extracting data from the TOML file diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 541059d0..457d268a 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -6,33 +6,43 @@ #include #include -namespace ModPlatform { -} // namespace ModPlatform - class QDir; +// Mod from launcher/minecraft/mod/Mod.h +class Mod; + class Packwiz { public: struct Mod { - QString name; - QString filename; + QString name {}; + QString filename {}; // FIXME: make side an enum - QString side = "both"; + QString side {"both"}; // [download] - QUrl url; + QUrl url {}; // FIXME: make hash-format an enum - QString hash_format; - QString hash; + QString hash_format {}; + QString hash {}; // [update] - ModPlatform::Provider provider; - QVariant file_id; - QVariant project_id; + ModPlatform::Provider provider {}; + QVariant file_id {}; + QVariant project_id {}; + + public: + // This is a heuristic, but should work for now. + auto isValid() const -> bool { return !name.isEmpty(); } }; - /* Generates the object representing the information in a mod.toml file via its common representation in the launcher */ + /* Generates the object representing the information in a mod.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.toml file via + * its common representation in the launcher. + * */ + static auto createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod; /* Updates the mod index for the provided mod. * This creates a new index if one does not exist already From fab4a7a6029beb60bade312ee89e649202d178de Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 16 Apr 2022 13:27:29 -0300 Subject: [PATCH 477/605] refactor: abstract metadata handling and clarify names --- launcher/CMakeLists.txt | 1 + launcher/MMCZip.cpp | 14 +++---- launcher/minecraft/MinecraftInstance.cpp | 10 ++--- launcher/minecraft/mod/MetadataHandler.h | 41 +++++++++++++++++++ launcher/minecraft/mod/Mod.cpp | 6 +-- launcher/minecraft/mod/Mod.h | 7 ++-- launcher/minecraft/mod/ModFolderModel.cpp | 2 +- .../mod/tasks/LocalModUpdateTask.cpp | 6 +-- .../minecraft/mod/tasks/ModFolderLoadTask.cpp | 4 +- launcher/modplatform/packwiz/Packwiz.cpp | 16 +++++--- launcher/modplatform/packwiz/Packwiz.h | 6 ++- 11 files changed, 82 insertions(+), 31 deletions(-) create mode 100644 launcher/minecraft/mod/MetadataHandler.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index b6df2851..03d68e66 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -322,6 +322,7 @@ set(MINECRAFT_SOURCES minecraft/WorldList.h minecraft/WorldList.cpp + minecraft/mod/MetadataHandler.h minecraft/mod/Mod.h minecraft/mod/Mod.cpp minecraft/mod/ModDetails.h diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 8591fcc0..627ceaf1 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -151,23 +151,23 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const continue; if (mod.type() == Mod::MOD_ZIPFILE) { - if (!mergeZipFiles(&zipOut, mod.filename(), addedFiles)) + if (!mergeZipFiles(&zipOut, mod.fileinfo(), addedFiles)) { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; return false; } } else if (mod.type() == Mod::MOD_SINGLEFILE) { // FIXME: buggy - does not work with addedFiles - auto filename = mod.filename(); + auto filename = mod.fileinfo(); if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; return false; } addedFiles.insert(filename.fileName()); @@ -176,7 +176,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const { // untested, but seems to be unused / not possible to reach // FIXME: buggy - does not work with addedFiles - auto filename = mod.filename(); + auto filename = mod.fileinfo(); QString what_to_zip = filename.absoluteFilePath(); QDir dir(what_to_zip); dir.cdUp(); @@ -193,7 +193,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; return false; } qDebug() << "Adding folder " << filename.fileName() << " from " @@ -204,7 +204,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const // Make sure we do not continue launching when something is missing or undefined... zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add unknown mod type" << mod.filename().fileName() << "to the jar."; + qCritical() << "Failed to add unknown mod type" << mod.fileinfo().fileName() << "to the jar."; return false; } } diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 61326fac..2f339014 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -659,23 +659,23 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << QString("%1:").arg(label); auto modList = model.allMods(); std::sort(modList.begin(), modList.end(), [](Mod &a, Mod &b) { - auto aName = a.filename().completeBaseName(); - auto bName = b.filename().completeBaseName(); + auto aName = a.fileinfo().completeBaseName(); + auto bName = b.fileinfo().completeBaseName(); return aName.localeAwareCompare(bName) < 0; }); for(auto & mod: modList) { if(mod.type() == Mod::MOD_FOLDER) { - out << u8" [📁] " + mod.filename().completeBaseName() + " (folder)"; + out << u8" [📁] " + mod.fileinfo().completeBaseName() + " (folder)"; continue; } if(mod.enabled()) { - out << u8" [✔️] " + mod.filename().completeBaseName(); + out << u8" [✔️] " + mod.fileinfo().completeBaseName(); } else { - out << u8" [❌] " + mod.filename().completeBaseName() + " (disabled)"; + out << u8" [❌] " + mod.fileinfo().completeBaseName() + " (disabled)"; } } diff --git a/launcher/minecraft/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h new file mode 100644 index 00000000..26b1f799 --- /dev/null +++ b/launcher/minecraft/mod/MetadataHandler.h @@ -0,0 +1,41 @@ +#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; + + 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) -> ModStruct + { + return Packwiz::V1::createModFormat(index_dir, internal_mod); + } + + static void update(QDir& index_dir, ModStruct& mod) + { + Packwiz::V1::updateModIndex(index_dir, mod); + } + + static void remove(QDir& index_dir, QString& mod_name) + { + Packwiz::V1::deleteModIndex(index_dir, mod_name); + } + + static auto get(QDir& index_dir, QString& mod_name) -> ModStruct + { + return Packwiz::V1::getIndexForMod(index_dir, mod_name); + } +}; diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 64c9ffb5..5b35156d 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -20,6 +20,7 @@ #include #include +#include "MetadataHandler.h" namespace { @@ -33,7 +34,7 @@ Mod::Mod(const QFileInfo& file) m_changedDateTime = file.lastModified(); } -Mod::Mod(const QDir& mods_dir, const Packwiz::Mod& metadata) +Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata) : m_file(mods_dir.absoluteFilePath(metadata.filename)) // It is weird, but name is not reliable for comparing with the JAR files name // FIXME: Maybe use hash when implemented? @@ -121,8 +122,7 @@ bool Mod::enable(bool value) bool Mod::destroy(QDir& index_dir) { - // Delete metadata - Packwiz::deleteModIndex(index_dir, m_name); + Metadata::remove(index_dir, m_name); m_type = MOD_UNKNOWN; return FS::deletePath(m_file.filePath()); diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 46bb1a59..fef8cbe4 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -21,7 +21,7 @@ #include #include "ModDetails.h" -#include "modplatform/packwiz/Packwiz.h" +#include "minecraft/mod/MetadataHandler.h" class Mod { @@ -37,9 +37,9 @@ public: Mod() = default; Mod(const QFileInfo &file); - explicit Mod(const QDir& mods_dir, const Packwiz::Mod& metadata); + explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata); - QFileInfo filename() const { return m_file; } + QFileInfo fileinfo() const { return m_file; } QDateTime dateTimeChanged() const { return m_changedDateTime; } QString internal_id() const { return m_internal_id; } ModType type() const { return m_type; } @@ -82,6 +82,7 @@ protected: QDateTime m_changedDateTime; QString m_internal_id; + /* Name as reported via the file name */ QString m_name; ModType m_type = MOD_UNKNOWN; bool m_from_metadata = false; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index e2e041eb..e034e35e 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -180,7 +180,7 @@ void ModFolderModel::resolveMod(Mod& m) return; } - auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.filename()); + auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.fileinfo()); auto result = task->result(); result->id = m.internal_id(); activeTickets.insert(nextResolutionTicket, result); diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index 63f5cf9a..8b6e8ec7 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -3,7 +3,7 @@ #include #include "FileSystem.h" -#include "modplatform/packwiz/Packwiz.h" +#include "minecraft/mod/MetadataHandler.h" 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) @@ -18,8 +18,8 @@ void LocalModUpdateTask::executeTask() { setStatus(tr("Updating index for mod:\n%1").arg(m_mod.name)); - auto pw_mod = Packwiz::createModFormat(m_index_dir, m_mod, m_mod_version); - Packwiz::updateModIndex(m_index_dir, pw_mod); + auto pw_mod = Metadata::create(m_index_dir, m_mod, m_mod_version); + Metadata::update(m_index_dir, pw_mod); emitSucceeded(); } diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index bf7b28d6..e94bdee9 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -1,7 +1,7 @@ #include "ModFolderLoadTask.h" #include -#include "modplatform/packwiz/Packwiz.h" +#include "minecraft/mod/MetadataHandler.h" ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir) : m_mods_dir(mods_dir), m_index_dir(index_dir), m_result(new Result()) @@ -17,7 +17,7 @@ void ModFolderLoadTask::run() continue; entry.chop(5); // Remove .toml at the end - Mod mod(m_mods_dir, Packwiz::getIndexForMod(m_index_dir, entry)); + Mod mod(m_mods_dir, Metadata::get(m_index_dir, entry)); m_result->mods[mod.internal_id()] = mod; } diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 445d64fb..27339c2d 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -9,13 +9,15 @@ #include "modplatform/ModIndex.h" #include "minecraft/mod/Mod.h" +namespace Packwiz { + // Helpers static inline QString indexFileName(QString const& mod_name) { return QString("%1.toml").arg(mod_name); } -auto Packwiz::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod +auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod { Mod mod; @@ -33,7 +35,7 @@ auto Packwiz::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pac return mod; } -auto Packwiz::createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod +auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod { auto mod_name = internal_mod.name(); @@ -44,7 +46,7 @@ auto Packwiz::createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod // Manually construct packwiz mod mod.name = internal_mod.name(); - mod.filename = internal_mod.filename().fileName(); + mod.filename = internal_mod.fileinfo().fileName(); // TODO: Have a mechanism for telling the UI subsystem that we want to gather user information // (i.e. which mod provider we want to use). Maybe an object parameter with a signal for that? @@ -52,7 +54,7 @@ auto Packwiz::createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod return mod; } -void Packwiz::updateModIndex(QDir& index_dir, Mod& mod) +void V1::updateModIndex(QDir& index_dir, Mod& mod) { if(!mod.isValid()){ qCritical() << QString("Tried to update metadata of an invalid mod!"); @@ -94,7 +96,7 @@ void Packwiz::updateModIndex(QDir& index_dir, Mod& mod) } } -void Packwiz::deleteModIndex(QDir& index_dir, QString& mod_name) +void V1::deleteModIndex(QDir& index_dir, QString& mod_name) { QFile index_file(index_dir.absoluteFilePath(indexFileName(mod_name))); @@ -108,7 +110,7 @@ void Packwiz::deleteModIndex(QDir& index_dir, QString& mod_name) } } -auto Packwiz::getIndexForMod(QDir& index_dir, QString& mod_name) -> Mod +auto V1::getIndexForMod(QDir& index_dir, QString& mod_name) -> Mod { Mod mod; @@ -201,3 +203,5 @@ auto Packwiz::getIndexForMod(QDir& index_dir, QString& mod_name) -> Mod return mod; } + +} // namespace Packwiz diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 457d268a..777a365f 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -11,7 +11,9 @@ class QDir; // Mod from launcher/minecraft/mod/Mod.h class Mod; -class Packwiz { +namespace Packwiz { + +class V1 { public: struct Mod { QString name {}; @@ -58,3 +60,5 @@ class Packwiz { * */ static auto getIndexForMod(QDir& index_dir, QString& mod_name) -> Mod; }; + +} // namespace Packwiz From 23febc6d94bcc5903a9863ba7b854b5091b0813b Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 17 Apr 2022 09:30:32 -0300 Subject: [PATCH 478/605] feat: cache metadata in ModDetails Allows for more easy access to the metadata by outside entities --- launcher/minecraft/mod/Mod.cpp | 14 ++++++++++++++ launcher/minecraft/mod/Mod.h | 14 ++++++++------ launcher/minecraft/mod/ModDetails.h | 7 +++++++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 5b35156d..46776239 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -55,6 +55,8 @@ Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata) m_from_metadata = true; m_enabled = true; m_changedDateTime = m_file.lastModified(); + + m_temp_metadata = std::make_shared(std::move(metadata)); } void Mod::repath(const QFileInfo& file) @@ -161,3 +163,15 @@ QStringList Mod::authors() const { return details().authors; } + +void Mod::finishResolvingWithDetails(std::shared_ptr details) +{ + m_resolving = false; + m_resolved = true; + m_localDetails = details; + + if (fromMetadata() && m_temp_metadata->isValid()) { + m_localDetails->metadata = m_temp_metadata; + m_temp_metadata.reset(); + } +} diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index fef8cbe4..0d49d94b 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -18,7 +18,6 @@ #include #include #include -#include #include "ModDetails.h" #include "minecraft/mod/MetadataHandler.h" @@ -55,6 +54,9 @@ public: QString description() const; QStringList authors() const; + const std::shared_ptr metadata() const { return details().metadata; }; + std::shared_ptr metadata() { return m_localDetails->metadata; }; + bool enable(bool value); // delete all the files of this mod @@ -71,11 +73,7 @@ public: m_resolving = resolving; m_resolutionTicket = resolutionTicket; } - void finishResolvingWithDetails(std::shared_ptr details){ - m_resolving = false; - m_resolved = true; - m_localDetails = details; - } + void finishResolvingWithDetails(std::shared_ptr details); protected: QFileInfo m_file; @@ -86,6 +84,10 @@ protected: QString m_name; ModType m_type = MOD_UNKNOWN; bool m_from_metadata = false; + + /* If the mod has metadata, this will be filled in the constructor, and passed to + * the ModDetails when calling finishResolvingWithDetails */ + std::shared_ptr m_temp_metadata; std::shared_ptr m_localDetails; bool m_enabled = true; diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index d8d4f66f..f9973fc2 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -1,8 +1,12 @@ #pragma once +#include + #include #include +#include "minecraft/mod/MetadataHandler.h" + struct ModDetails { /* Mod ID as defined in the ModLoader-specific metadata */ @@ -25,4 +29,7 @@ struct ModDetails /* List of the author's names */ QStringList authors; + + /* Metadata information, if any */ + std::shared_ptr metadata; }; From 4439666e67573a6a36af981fdc68410fdf9e4f9f Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 17 Apr 2022 10:19:23 -0300 Subject: [PATCH 479/605] feat: allow disabling mod metadata usage --- launcher/Application.cpp | 3 + launcher/minecraft/mod/Mod.cpp | 6 +- .../mod/tasks/LocalModUpdateTask.cpp | 6 ++ .../minecraft/mod/tasks/ModFolderLoadTask.cpp | 28 +++--- .../minecraft/mod/tasks/ModFolderLoadTask.h | 4 + launcher/ui/pages/global/LauncherPage.cpp | 12 +++ launcher/ui/pages/global/LauncherPage.h | 1 + launcher/ui/pages/global/LauncherPage.ui | 87 ++++++++++++------- 8 files changed, 107 insertions(+), 40 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ba4096b6..ae4cbcf8 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -643,6 +643,9 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Minecraft launch method m_settings->registerSetting("MCLaunchMethod", "LauncherPart"); + // Minecraft mods + m_settings->registerSetting("DontUseModMetadata", false); + // Minecraft offline player name m_settings->registerSetting("LastOfflinePlayerName", ""); diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 46776239..7b560845 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -124,7 +124,11 @@ bool Mod::enable(bool value) bool Mod::destroy(QDir& index_dir) { - Metadata::remove(index_dir, m_name); + auto n = name(); + // FIXME: This can fail to remove the metadata if the + // "DontUseModMetadata" setting is on, since there could + // be a name mismatch! + Metadata::remove(index_dir, n); m_type = MOD_UNKNOWN; return FS::deletePath(m_file.filePath()); diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index 8b6e8ec7..3c9b76a8 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -2,6 +2,7 @@ #include +#include "Application.h" #include "FileSystem.h" #include "minecraft/mod/MetadataHandler.h" @@ -18,6 +19,11 @@ void LocalModUpdateTask::executeTask() { setStatus(tr("Updating index for mod:\n%1").arg(m_mod.name)); + if(APPLICATION->settings()->get("DontUseModMetadata").toBool()){ + emitSucceeded(); + return; + } + auto pw_mod = Metadata::create(m_index_dir, m_mod, m_mod_version); Metadata::update(m_index_dir, pw_mod); diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index e94bdee9..5afbb08a 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -1,6 +1,7 @@ #include "ModFolderLoadTask.h" #include +#include "Application.h" #include "minecraft/mod/MetadataHandler.h" ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir) @@ -9,16 +10,9 @@ ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir) void ModFolderLoadTask::run() { - // Read metadata first - m_index_dir.refresh(); - for (auto entry : m_index_dir.entryList()) { - // QDir::Filter::NoDotAndDotDot seems to exclude all files for some reason... - if (entry == "." || entry == "..") - continue; - - entry.chop(5); // Remove .toml at the end - Mod mod(m_mods_dir, Metadata::get(m_index_dir, entry)); - m_result->mods[mod.internal_id()] = mod; + if (!APPLICATION->settings()->get("DontUseModMetadata").toBool()) { + // Read metadata first + getFromMetadata(); } // Read JAR files that don't have metadata @@ -31,3 +25,17 @@ void ModFolderLoadTask::run() emit succeeded(); } + +void ModFolderLoadTask::getFromMetadata() +{ + m_index_dir.refresh(); + for (auto entry : m_index_dir.entryList()) { + // QDir::Filter::NoDotAndDotDot seems to exclude all files for some reason... + if (entry == "." || entry == "..") + continue; + + entry.chop(5); // Remove .toml at the end + Mod mod(m_mods_dir, Metadata::get(m_index_dir, entry)); + m_result->mods[mod.internal_id()] = mod; + } +} diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h index bb66022a..ba997874 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h @@ -24,6 +24,10 @@ public: void run(); signals: void succeeded(); + +private: + void getFromMetadata(); + private: QDir& m_mods_dir, m_index_dir; ResultPtr m_result; diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index af2e2cd1..8754c0ec 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -184,6 +184,11 @@ void LauncherPage::on_modsDirBrowseBtn_clicked() } } +void LauncherPage::on_metadataDisableBtn_clicked() +{ + ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); +} + void LauncherPage::refreshUpdateChannelList() { // Stop listening for selection changes. It's going to change a lot while we update it and @@ -338,6 +343,9 @@ void LauncherPage::applySettings() s->set("InstSortMode", "Name"); break; } + + // Mods + s->set("DontUseModMetadata", ui->metadataDisableBtn->isChecked()); } void LauncherPage::loadSettings() { @@ -440,6 +448,10 @@ void LauncherPage::loadSettings() { ui->sortByNameBtn->setChecked(true); } + + // Mods + ui->metadataDisableBtn->setChecked(s->get("DontUseModMetadata").toBool()); + ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); } void LauncherPage::refreshFontPreview() diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index bbf5d2fe..f38c922e 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -88,6 +88,7 @@ slots: void on_instDirBrowseBtn_clicked(); void on_modsDirBrowseBtn_clicked(); void on_iconsDirBrowseBtn_clicked(); + void on_metadataDisableBtn_clicked(); /*! * Updates the list of update channels in the combo box. diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index ae7eb73f..417bbe05 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -94,19 +94,13 @@ Folders - - + + - I&nstances: - - - instDirTextBox + ... - - - @@ -114,28 +108,15 @@ - - - - &Mods: - - - modsDirTextBox - - - - - - - - + + ... - - + + @@ -147,10 +128,58 @@ - - + + + + + - ... + I&nstances: + + + instDirTextBox + + + + + + + + + + &Mods: + + + modsDirTextBox + + + + + + + + + + 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 upcoming QoL features, such as mod updating!</span></p></body></html> + + + true From d7f6b3699074b268fd554bd1eb9da68f1e533355 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 17 Apr 2022 11:40:41 -0300 Subject: [PATCH 480/605] test+fix: add basic tests and fix issues with it --- launcher/CMakeLists.txt | 6 ++ launcher/minecraft/mod/Mod.cpp | 7 +- launcher/minecraft/mod/Mod.h | 1 - .../minecraft/mod/tasks/ModFolderLoadTask.cpp | 10 ++- launcher/modplatform/ModIndex.h | 2 +- launcher/modplatform/packwiz/Packwiz.cpp | 76 +++++++++++------- launcher/modplatform/packwiz/Packwiz.h | 11 ++- launcher/modplatform/packwiz/Packwiz_test.cpp | 68 ++++++++++++++++ .../packwiz/testdata/borderless-mining.toml | Bin 0 -> 431 bytes .../screenshot-to-clipboard-fabric.toml | Bin 0 -> 323 bytes 10 files changed, 142 insertions(+), 39 deletions(-) create mode 100644 launcher/modplatform/packwiz/Packwiz_test.cpp create mode 100644 launcher/modplatform/packwiz/testdata/borderless-mining.toml create mode 100644 launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.toml diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 03d68e66..6c7b5e43 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -551,6 +551,12 @@ set(PACKWIZ_SOURCES modplatform/packwiz/Packwiz.cpp ) +add_unit_test(Packwiz + SOURCES modplatform/packwiz/Packwiz_test.cpp + DATA modplatform/packwiz/testdata + LIBS Launcher_logic + ) + set(TECHNIC_SOURCES modplatform/technic/SingleZipPackInstallTask.h modplatform/technic/SingleZipPackInstallTask.cpp diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 7b560845..ef3699e8 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -20,6 +20,8 @@ #include #include + +#include "Application.h" #include "MetadataHandler.h" namespace { @@ -174,8 +176,7 @@ void Mod::finishResolvingWithDetails(std::shared_ptr details) m_resolved = true; m_localDetails = details; - if (fromMetadata() && m_temp_metadata->isValid()) { - m_localDetails->metadata = m_temp_metadata; - m_temp_metadata.reset(); + if (fromMetadata() && m_temp_metadata->isValid() && m_localDetails.get()) { + m_localDetails->metadata.swap(m_temp_metadata); } } diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 0d49d94b..1e7ed1ed 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -20,7 +20,6 @@ #include #include "ModDetails.h" -#include "minecraft/mod/MetadataHandler.h" class Mod { diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 5afbb08a..03a17461 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -34,8 +34,14 @@ void ModFolderLoadTask::getFromMetadata() if (entry == "." || entry == "..") continue; - entry.chop(5); // Remove .toml at the end - Mod mod(m_mods_dir, Metadata::get(m_index_dir, entry)); + auto metadata = Metadata::get(m_index_dir, entry); + // TODO: Don't simply return. Instead, show to the user that the metadata is there, but + // it's not currently 'installed' (i.e. there's no JAR file yet). + if(!metadata.isValid()){ + return; + } + + Mod mod(m_mods_dir, metadata); m_result->mods[mod.internal_id()] = mod; } } diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index c5329772..ee623b78 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -19,7 +19,7 @@ class ProviderCapabilities { { switch(p){ case Provider::MODRINTH: - return "sha256"; + return "sha512"; case Provider::FLAME: return "murmur2"; } diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 27339c2d..8fd74a3e 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -14,6 +14,8 @@ namespace Packwiz { // Helpers static inline QString indexFileName(QString const& mod_name) { + if(mod_name.endsWith(".toml")) + return mod_name; return QString("%1.toml").arg(mod_name); } @@ -91,8 +93,16 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) in_stream << QString("\n[update]\n"); in_stream << QString("[update.%1]\n").arg(ModPlatform::ProviderCapabilities::providerName(mod.provider)); - addToStream("file-id", mod.file_id.toString()); - addToStream("project-id", mod.project_id.toString()); + switch(mod.provider){ + case(ModPlatform::Provider::FLAME): + in_stream << QString("file-id = %1\n").arg(mod.file_id.toString()); + in_stream << QString("project-id = %1\n").arg(mod.project_id.toString()); + break; + case(ModPlatform::Provider::MODRINTH): + addToStream("mod-id", mod.mod_id().toString()); + addToStream("version", mod.version().toString()); + break; + } } } @@ -110,18 +120,44 @@ void V1::deleteModIndex(QDir& index_dir, QString& mod_name) } } -auto V1::getIndexForMod(QDir& index_dir, QString& mod_name) -> Mod +// Helper functions for extracting data from the TOML file +static auto stringEntry(toml_table_t* parent, const char* entry_name) -> QString +{ + toml_datum_t var = toml_string_in(parent, entry_name); + if (!var.ok) { + qCritical() << QString("Failed to read str property '%1' in mod metadata.").arg(entry_name); + return {}; + } + + QString tmp = var.u.s; + free(var.u.s); + + return tmp; +} + +static auto intEntry(toml_table_t* parent, const char* entry_name) -> int +{ + toml_datum_t var = toml_int_in(parent, entry_name); + if (!var.ok) { + qCritical() << QString("Failed to read int property '%1' in mod metadata.").arg(entry_name); + return {}; + } + + return var.u.i; +} + +auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod { Mod mod; - QFile index_file(index_dir.absoluteFilePath(indexFileName(mod_name))); + QFile index_file(index_dir.absoluteFilePath(indexFileName(index_file_name))); if (!index_file.exists()) { - qWarning() << QString("Tried to get a non-existent mod metadata for %1").arg(mod_name); + qWarning() << QString("Tried to get a non-existent mod metadata for %1").arg(index_file_name); return {}; } if (!index_file.open(QIODevice::ReadOnly)) { - qWarning() << QString("Failed to open mod metadata for %1").arg(mod_name); + qWarning() << QString("Failed to open mod metadata for %1").arg(index_file_name); return {}; } @@ -136,29 +172,9 @@ auto V1::getIndexForMod(QDir& index_dir, QString& mod_name) -> Mod qCritical() << QString("Could not open file %1!").arg(indexFileName(mod.name)); return {}; } - - // Helper function for extracting data from the TOML file - auto stringEntry = [&](toml_table_t* parent, const char* entry_name) -> QString { - toml_datum_t var = toml_string_in(parent, entry_name); - if (!var.ok) { - qCritical() << QString("Failed to read property '%1' in mod metadata.").arg(entry_name); - return {}; - } - - QString tmp = var.u.s; - free(var.u.s); - - return tmp; - }; - + { // Basic info mod.name = stringEntry(table, "name"); - // Basic sanity check - if (mod.name != mod_name) { - qCritical() << QString("Name mismatch in mod metadata:\nExpected:%1\nGot:%2").arg(mod_name, mod.name); - return {}; - } - mod.filename = stringEntry(table, "filename"); mod.side = stringEntry(table, "side"); } @@ -188,15 +204,17 @@ auto V1::getIndexForMod(QDir& index_dir, QString& mod_name) -> Mod toml_table_t* mod_provider_table; if ((mod_provider_table = toml_table_in(update_table, ProviderCaps::providerName(Provider::FLAME)))) { mod.provider = Provider::FLAME; + mod.file_id = intEntry(mod_provider_table, "file-id"); + mod.project_id = intEntry(mod_provider_table, "project-id"); } else if ((mod_provider_table = toml_table_in(update_table, ProviderCaps::providerName(Provider::MODRINTH)))) { mod.provider = Provider::MODRINTH; + mod.mod_id() = stringEntry(mod_provider_table, "mod-id"); + mod.version() = stringEntry(mod_provider_table, "version"); } else { qCritical() << QString("No mod provider on mod metadata!"); return {}; } - mod.file_id = stringEntry(mod_provider_table, "file-id"); - mod.project_id = stringEntry(mod_provider_table, "project-id"); } toml_free(table); diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 777a365f..69125dbc 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -33,8 +33,13 @@ class V1 { QVariant project_id {}; public: - // This is a heuristic, but should work for now. - auto isValid() const -> bool { return !name.isEmpty(); } + // This is a totally heuristic, but should work for now. + auto isValid() const -> bool { return !name.isEmpty() && !project_id.isNull(); } + + // Different providers can use different names for the same thing + // Modrinth-specific + auto mod_id() -> QVariant& { return project_id; } + auto version() -> QVariant& { return file_id; } }; /* Generates the object representing the information in a mod.toml file via @@ -58,7 +63,7 @@ class V1 { /* Gets the metadata for a mod with a particular name. * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ - static auto getIndexForMod(QDir& index_dir, QString& mod_name) -> Mod; + static auto getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod; }; } // namespace Packwiz diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/launcher/modplatform/packwiz/Packwiz_test.cpp new file mode 100644 index 00000000..2e61c167 --- /dev/null +++ b/launcher/modplatform/packwiz/Packwiz_test.cpp @@ -0,0 +1,68 @@ +#include +#include + +#include "TestUtil.h" +#include "Packwiz.h" + +class PackwizTest : public QObject { + Q_OBJECT + + private slots: + // Files taken from https://github.com/packwiz/packwiz-example-pack + void loadFromFile_Modrinth() + { + QString source = QFINDTESTDATA("testdata"); + + QDir index_dir(source); + QString name_mod("borderless-mining.toml"); + QVERIFY(index_dir.entryList().contains(name_mod)); + + auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod); + + QVERIFY(metadata.isValid()); + + QCOMPARE(metadata.name, "Borderless Mining"); + QCOMPARE(metadata.filename, "borderless-mining-1.1.1+1.18.jar"); + QCOMPARE(metadata.side, "client"); + + 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"); + QCOMPARE(metadata.hash, "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d"); + + QCOMPARE(metadata.provider, ModPlatform::Provider::MODRINTH); + QCOMPARE(metadata.version(), "ug2qKTPR"); + QCOMPARE(metadata.mod_id(), "kYq5qkSL"); + } + + void loadFromFile_Curseforge() + { + QString source = QFINDTESTDATA("testdata"); + + QDir index_dir(source); + QString name_mod("screenshot-to-clipboard-fabric.toml"); + QVERIFY(index_dir.entryList().contains(name_mod)); + + // Try without the .toml at the end + name_mod.chop(5); + + auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod); + + QVERIFY(metadata.isValid()); + + QCOMPARE(metadata.name, "Screenshot to Clipboard (Fabric)"); + QCOMPARE(metadata.filename, "screenshot-to-clipboard-1.0.7-fabric.jar"); + QCOMPARE(metadata.side, "both"); + + QCOMPARE(metadata.url, QUrl("https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar")); + QCOMPARE(metadata.hash_format, "murmur2"); + QCOMPARE(metadata.hash, "1781245820"); + + QCOMPARE(metadata.provider, ModPlatform::Provider::FLAME); + QCOMPARE(metadata.file_id, 3509043); + QCOMPARE(metadata.project_id, 327154); + } +}; + +QTEST_GUILESS_MAIN(PackwizTest) + +#include "Packwiz_test.moc" diff --git a/launcher/modplatform/packwiz/testdata/borderless-mining.toml b/launcher/modplatform/packwiz/testdata/borderless-mining.toml new file mode 100644 index 0000000000000000000000000000000000000000..16545fd43676d21c043ae9600d9a17eb68101652 GIT binary patch literal 431 zcmaiw%TB{E5JmU?iYi-_haYjBN^IBy5&|{|buspg8`H#T;}m|Mq-x6&StEJwmFCWz z2tBRtSJ}fbB8?rTw0aIP#9hXG=qO%nd$aTYZ0Ed~-`!lM_<}KGDd2gK>jK3oW9$=$ zpV$q6TXq_|C8M3DL)w(3!&vkKjv-EM;fB6Mn4sK$9P8u$?Wz2xF@+(f@-L$NKfi_4 z=6)D^n3k;6Ld`|S7J2EN@uZ2@hy+q-ZHy3zXvHj=np5p7X{55Gth0i=Z(N12_UJ03 zp|RQ#;M$PnpcG2$w3f1V7C7fh5mi#IoyJ-!?YRXlwUCuos%fm`#^3_vbeIpN?e%kG cuw^riJm9kDl|sfY7#8ug6UWE*m)DH_0+y+Si~s-t literal 0 HcmV?d00001 diff --git a/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.toml b/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.toml new file mode 100644 index 0000000000000000000000000000000000000000..87d70ada52ca3e8b709d08a75c4ccbc15222c2ac GIT binary patch literal 323 zcma)%y>7%H5QKZ40#PLe;9vPJQmROKfs~O84C{lF&04TlUO&dVOC4#8)o5nF*=Sba z?_7M@1Q4@F;)MKT3EPAwIsWo#rWEX}U~^a?KHT}wEeWN4x@D~@HOTplsJlsm<>1cy z6OtE{ePr4*~{b7YN!C# zJsr~sR`ep&!=-Mz{?b&X(7riCFg_P$_mtu6F`h5W;EqsfQFSfb65hemLu`h+@7OQN C250L4 literal 0 HcmV?d00001 From ba50765c306d2907e411bc0ed9a10d990cf42fd3 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 19 Apr 2022 20:19:51 -0300 Subject: [PATCH 481/605] tidy: apply clang-tidy to some files Mostly the ones created in this PR + Mod.h / Mod.cpp / ModDetails.h --- launcher/minecraft/mod/Mod.cpp | 16 ++++---- launcher/minecraft/mod/Mod.h | 40 +++++++++---------- .../mod/tasks/LocalModUpdateTask.cpp | 2 +- .../minecraft/mod/tasks/LocalModUpdateTask.h | 6 +-- launcher/modplatform/packwiz/Packwiz.cpp | 9 +++-- launcher/modplatform/packwiz/Packwiz_test.cpp | 2 +- 6 files changed, 38 insertions(+), 37 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index ef3699e8..992b91dc 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -93,7 +93,7 @@ void Mod::repath(const QFileInfo& file) } } -bool Mod::enable(bool value) +auto Mod::enable(bool value) -> bool { if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) return false; @@ -124,7 +124,7 @@ bool Mod::enable(bool value) return true; } -bool Mod::destroy(QDir& index_dir) +auto Mod::destroy(QDir& index_dir) -> bool { auto n = name(); // FIXME: This can fail to remove the metadata if the @@ -136,12 +136,12 @@ bool Mod::destroy(QDir& index_dir) return FS::deletePath(m_file.filePath()); } -const ModDetails& Mod::details() const +auto Mod::details() const -> const ModDetails& { return m_localDetails ? *m_localDetails : invalidDetails; } -QString Mod::name() const +auto Mod::name() const -> QString { auto d_name = details().name; if (!d_name.isEmpty()) { @@ -150,22 +150,22 @@ QString Mod::name() const return m_name; } -QString Mod::version() const +auto Mod::version() const -> QString { return details().version; } -QString Mod::homeurl() const +auto Mod::homeurl() const -> QString { return details().homeurl; } -QString Mod::description() const +auto Mod::description() const -> QString { return details().description; } -QStringList Mod::authors() const +auto Mod::authors() const -> QStringList { return details().authors; } diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 1e7ed1ed..3a0ccfa6 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -37,36 +37,36 @@ public: Mod(const QFileInfo &file); explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata); - QFileInfo fileinfo() const { return m_file; } - QDateTime dateTimeChanged() const { return m_changedDateTime; } - QString internal_id() const { return m_internal_id; } - ModType type() const { return m_type; } - bool fromMetadata() const { return m_from_metadata; } - bool enabled() const { return m_enabled; } + auto fileinfo() const -> QFileInfo { return m_file; } + auto dateTimeChanged() const -> QDateTime { return m_changedDateTime; } + auto internal_id() const -> QString { return m_internal_id; } + auto type() const -> ModType { return m_type; } + auto fromMetadata() const -> bool { return m_from_metadata; } + auto enabled() const -> bool { return m_enabled; } - bool valid() const { return m_type != MOD_UNKNOWN; } + auto valid() const -> bool { return m_type != MOD_UNKNOWN; } - const ModDetails& details() const; - QString name() const; - QString version() const; - QString homeurl() const; - QString description() const; - QStringList authors() const; + auto details() const -> const ModDetails&; + auto name() const -> QString; + auto version() const -> QString; + auto homeurl() const -> QString; + auto description() const -> QString; + auto authors() const -> QStringList; - const std::shared_ptr metadata() const { return details().metadata; }; - std::shared_ptr metadata() { return m_localDetails->metadata; }; + auto metadata() const -> const std::shared_ptr { return details().metadata; }; + auto metadata() -> std::shared_ptr { return m_localDetails->metadata; }; - bool enable(bool value); + auto enable(bool value) -> bool; // delete all the files of this mod - bool destroy(QDir& index_dir); + auto destroy(QDir& index_dir) -> bool; // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) void repath(const QFileInfo &file); - bool shouldResolve() const { return !m_resolving && !m_resolved; } - bool isResolving() const { return m_resolving; } - int resolutionTicket() const { return m_resolutionTicket; } + auto shouldResolve() const -> bool { return !m_resolving && !m_resolved; } + auto isResolving() const -> bool { return m_resolving; } + auto resolutionTicket() const -> int { return m_resolutionTicket; } void setResolving(bool resolving, int resolutionTicket) { m_resolving = resolving; diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index 3c9b76a8..47207ada 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -30,7 +30,7 @@ void LocalModUpdateTask::executeTask() emitSucceeded(); } -bool LocalModUpdateTask::abort() +auto LocalModUpdateTask::abort() -> bool { emitAborted(); return true; diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h index 866089e9..15591b21 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h @@ -2,8 +2,8 @@ #include -#include "tasks/Task.h" #include "modplatform/ModIndex.h" +#include "tasks/Task.h" class LocalModUpdateTask : public Task { Q_OBJECT @@ -12,8 +12,8 @@ class LocalModUpdateTask : public Task { explicit LocalModUpdateTask(QDir mods_dir, ModPlatform::IndexedPack& mod, ModPlatform::IndexedVersion& mod_version); - bool canAbort() const override { return true; } - bool abort() override; + auto canAbort() const -> bool override { return true; } + auto abort() -> bool override; protected slots: //! Entry point for tasks. diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 8fd74a3e..978be462 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -6,13 +6,13 @@ #include "toml.h" -#include "modplatform/ModIndex.h" #include "minecraft/mod/Mod.h" +#include "modplatform/ModIndex.h" namespace Packwiz { // Helpers -static inline QString indexFileName(QString const& mod_name) +static inline auto indexFileName(QString const& mod_name) -> QString { if(mod_name.endsWith(".toml")) return mod_name; @@ -161,8 +161,9 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod return {}; } - toml_table_t* table; + toml_table_t* table = nullptr; + // NOLINTNEXTLINE(modernize-avoid-c-arrays) char errbuf[200]; table = toml_parse(index_file.readAll().data(), errbuf, sizeof(errbuf)); @@ -201,7 +202,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod return {}; } - toml_table_t* mod_provider_table; + toml_table_t* mod_provider_table = nullptr; if ((mod_provider_table = toml_table_in(update_table, ProviderCaps::providerName(Provider::FLAME)))) { mod.provider = Provider::FLAME; mod.file_id = intEntry(mod_provider_table, "file-id"); diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/launcher/modplatform/packwiz/Packwiz_test.cpp index 2e61c167..08de332d 100644 --- a/launcher/modplatform/packwiz/Packwiz_test.cpp +++ b/launcher/modplatform/packwiz/Packwiz_test.cpp @@ -1,8 +1,8 @@ #include #include -#include "TestUtil.h" #include "Packwiz.h" +#include "TestUtil.h" class PackwizTest : public QObject { Q_OBJECT From a99858c64d275303a9f91912a2732746ef6a3c8a Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 19 Apr 2022 21:10:12 -0300 Subject: [PATCH 482/605] refactor: move code out of ModIndex.h Now it's in ModIndex.cpp --- launcher/CMakeLists.txt | 3 +++ launcher/modplatform/ModIndex.cpp | 24 +++++++++++++++++++++ launcher/modplatform/ModIndex.h | 22 ++----------------- launcher/modplatform/flame/FlameAPI.h | 1 + launcher/modplatform/modrinth/ModrinthAPI.h | 1 + launcher/modplatform/packwiz/Packwiz.cpp | 11 +++++----- 6 files changed, 37 insertions(+), 25 deletions(-) create mode 100644 launcher/modplatform/ModIndex.cpp diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6c7b5e43..1bab7ecb 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -500,6 +500,9 @@ set(META_SOURCES ) set(API_SOURCES + modplatform/ModIndex.h + modplatform/ModIndex.cpp + modplatform/ModAPI.h modplatform/flame/FlameAPI.h diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp new file mode 100644 index 00000000..eb8be992 --- /dev/null +++ b/launcher/modplatform/ModIndex.cpp @@ -0,0 +1,24 @@ +#include "modplatform/ModIndex.h" + +namespace ModPlatform{ + +auto ProviderCapabilities::name(Provider p) -> const char* +{ + switch(p){ + case Provider::MODRINTH: + return "modrinth"; + case Provider::FLAME: + return "curseforge"; + } +} +auto ProviderCapabilities::hashType(Provider p) -> QString +{ + switch(p){ + case Provider::MODRINTH: + return "sha512"; + case Provider::FLAME: + return "murmur2"; + } +} + +} // namespace ModPlatform diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index ee623b78..bb5c7c9d 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -15,26 +15,8 @@ enum class Provider{ class ProviderCapabilities { public: - static QString hashType(Provider p) - { - switch(p){ - case Provider::MODRINTH: - return "sha512"; - case Provider::FLAME: - return "murmur2"; - } - return ""; - } - static const char* providerName(Provider p) - { - switch(p){ - case Provider::MODRINTH: - return "modrinth"; - case Provider::FLAME: - return "curseforge"; - } - return ""; - } + auto name(Provider) -> const char*; + auto hashType(Provider) -> QString; }; struct ModpackAuthor { diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 8bb33d47..e31cf0a1 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -1,5 +1,6 @@ #pragma once +#include "modplatform/ModIndex.h" #include "modplatform/helpers/NetworkModAPI.h" class FlameAPI : public NetworkModAPI { diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 79bc5175..f9d35fcd 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -20,6 +20,7 @@ #include "BuildConfig.h" #include "modplatform/ModAPI.h" +#include "modplatform/ModIndex.h" #include "modplatform/helpers/NetworkModAPI.h" #include diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 978be462..872da9b1 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -19,6 +19,8 @@ static inline auto indexFileName(QString const& mod_name) -> QString return QString("%1.toml").arg(mod_name); } +static ModPlatform::ProviderCapabilities ProviderCaps; + auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod { Mod mod; @@ -27,7 +29,7 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo mod.filename = mod_version.fileName; mod.url = mod_version.downloadUrl; - mod.hash_format = ModPlatform::ProviderCapabilities::hashType(mod_pack.provider); + mod.hash_format = ProviderCaps.hashType(mod_pack.provider); mod.hash = ""; // FIXME mod.provider = mod_pack.provider; @@ -92,7 +94,7 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) addToStream("hash", mod.hash); in_stream << QString("\n[update]\n"); - in_stream << QString("[update.%1]\n").arg(ModPlatform::ProviderCapabilities::providerName(mod.provider)); + in_stream << QString("[update.%1]\n").arg(ProviderCaps.name(mod.provider)); switch(mod.provider){ case(ModPlatform::Provider::FLAME): in_stream << QString("file-id = %1\n").arg(mod.file_id.toString()); @@ -193,7 +195,6 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod } { // [update] info - using ProviderCaps = ModPlatform::ProviderCapabilities; using Provider = ModPlatform::Provider; toml_table_t* update_table = toml_table_in(table, "update"); @@ -203,11 +204,11 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod } toml_table_t* mod_provider_table = nullptr; - if ((mod_provider_table = toml_table_in(update_table, ProviderCaps::providerName(Provider::FLAME)))) { + if ((mod_provider_table = toml_table_in(update_table, ProviderCaps.name(Provider::FLAME)))) { mod.provider = Provider::FLAME; mod.file_id = intEntry(mod_provider_table, "file-id"); mod.project_id = intEntry(mod_provider_table, "project-id"); - } else if ((mod_provider_table = toml_table_in(update_table, ProviderCaps::providerName(Provider::MODRINTH)))) { + } else if ((mod_provider_table = toml_table_in(update_table, ProviderCaps.name(Provider::MODRINTH)))) { mod.provider = Provider::MODRINTH; mod.mod_id() = stringEntry(mod_provider_table, "mod-id"); mod.version() = stringEntry(mod_provider_table, "version"); From 96e36f060443cbfa6d58df2adca3c8605851b4a3 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 20 Apr 2022 18:45:39 -0300 Subject: [PATCH 483/605] refactor: make mod metadata presence (or lack of) easier to find out --- launcher/minecraft/mod/Mod.cpp | 28 +++++++++++++++++-- launcher/minecraft/mod/Mod.h | 6 ++-- launcher/minecraft/mod/ModDetails.h | 9 ++++++ .../minecraft/mod/tasks/ModFolderLoadTask.cpp | 8 +++++- launcher/modplatform/ModIndex.cpp | 2 ++ launcher/modplatform/packwiz/Packwiz.cpp | 9 ++---- 6 files changed, 49 insertions(+), 13 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 992b91dc..261ae9d2 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -54,7 +54,6 @@ Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata) m_type = MOD_SINGLEFILE; } - m_from_metadata = true; m_enabled = true; m_changedDateTime = m_file.lastModified(); @@ -117,13 +116,27 @@ auto Mod::enable(bool value) -> bool return false; } - if (!fromMetadata()) + if (status() == ModStatus::NoMetadata) repath(QFileInfo(path)); m_enabled = value; return true; } +void Mod::setStatus(ModStatus status) +{ + if(m_localDetails.get()) + m_localDetails->status = status; +} +void Mod::setMetadata(Metadata::ModStruct* metadata) +{ + if(status() == ModStatus::NoMetadata) + setStatus(ModStatus::Installed); + + if(m_localDetails.get()) + m_localDetails->metadata.reset(metadata); +} + auto Mod::destroy(QDir& index_dir) -> bool { auto n = name(); @@ -170,13 +183,22 @@ auto Mod::authors() const -> QStringList return details().authors; } +auto Mod::status() const -> ModStatus +{ + return details().status; +} + void Mod::finishResolvingWithDetails(std::shared_ptr details) { m_resolving = false; m_resolved = true; m_localDetails = details; - if (fromMetadata() && m_temp_metadata->isValid() && m_localDetails.get()) { + if (status() != ModStatus::NoMetadata + && m_temp_metadata.get() + && m_temp_metadata->isValid() && + m_localDetails.get()) { + m_localDetails->metadata.swap(m_temp_metadata); } } diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 3a0ccfa6..58c7a80f 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -41,7 +41,6 @@ public: auto dateTimeChanged() const -> QDateTime { return m_changedDateTime; } auto internal_id() const -> QString { return m_internal_id; } auto type() const -> ModType { return m_type; } - auto fromMetadata() const -> bool { return m_from_metadata; } auto enabled() const -> bool { return m_enabled; } auto valid() const -> bool { return m_type != MOD_UNKNOWN; } @@ -52,10 +51,14 @@ public: auto homeurl() const -> QString; auto description() const -> QString; auto authors() const -> QStringList; + auto status() const -> ModStatus; auto metadata() const -> const std::shared_ptr { return details().metadata; }; auto metadata() -> std::shared_ptr { return m_localDetails->metadata; }; + void setStatus(ModStatus status); + void setMetadata(Metadata::ModStruct* metadata); + auto enable(bool value) -> bool; // delete all the files of this mod @@ -82,7 +85,6 @@ protected: /* Name as reported via the file name */ QString m_name; ModType m_type = MOD_UNKNOWN; - bool m_from_metadata = false; /* If the mod has metadata, this will be filled in the constructor, and passed to * the ModDetails when calling finishResolvingWithDetails */ diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index f9973fc2..75ffea32 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -7,6 +7,12 @@ #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 +}; + struct ModDetails { /* Mod ID as defined in the ModLoader-specific metadata */ @@ -30,6 +36,9 @@ struct ModDetails /* List of the author's names */ QStringList authors; + /* Installation status of the mod */ + ModStatus status; + /* Metadata information, if any */ std::shared_ptr metadata; }; diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 03a17461..addb0dd8 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -19,8 +19,13 @@ void ModFolderLoadTask::run() m_mods_dir.refresh(); for (auto entry : m_mods_dir.entryInfoList()) { Mod mod(entry); - if (!m_result->mods.contains(mod.internal_id())) + if(m_result->mods.contains(mod.internal_id())){ + m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); + } + else { m_result->mods[mod.internal_id()] = mod; + m_result->mods[mod.internal_id()].setStatus(ModStatus::NoMetadata); + } } emit succeeded(); @@ -42,6 +47,7 @@ void ModFolderLoadTask::getFromMetadata() } Mod mod(m_mods_dir, metadata); + mod.setStatus(ModStatus::NotInstalled); m_result->mods[mod.internal_id()] = mod; } } diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index eb8be992..b3c057fb 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -10,6 +10,7 @@ auto ProviderCapabilities::name(Provider p) -> const char* case Provider::FLAME: return "curseforge"; } + return {}; } auto ProviderCapabilities::hashType(Provider p) -> QString { @@ -19,6 +20,7 @@ auto ProviderCapabilities::hashType(Provider p) -> QString case Provider::FLAME: return "murmur2"; } + return {}; } } // namespace ModPlatform diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 872da9b1..50f87c24 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -48,14 +48,9 @@ auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod if(mod.isValid()) return mod; - // Manually construct packwiz mod - mod.name = internal_mod.name(); - mod.filename = internal_mod.fileinfo().fileName(); + qWarning() << QString("Tried to create mod metadata with a Mod without metadata!"); - // TODO: Have a mechanism for telling the UI subsystem that we want to gather user information - // (i.e. which mod provider we want to use). Maybe an object parameter with a signal for that? - - return mod; + return {}; } void V1::updateModIndex(QDir& index_dir, Mod& mod) From e17b6804a7424dd5161662c4ef92972f3311675c Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 21 Apr 2022 15:45:20 -0300 Subject: [PATCH 484/605] fix: implement PR suggestions Some stylistic changes, and get hashes from the mod providers when building the metadata. --- launcher/Application.cpp | 2 +- launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp | 11 +++-------- launcher/modplatform/ModIndex.h | 3 ++- launcher/modplatform/flame/FlameModIndex.cpp | 8 ++++++++ launcher/modplatform/modrinth/ModrinthPackIndex.cpp | 4 ++++ launcher/modplatform/packwiz/Packwiz.cpp | 2 +- launcher/ui/pages/global/LauncherPage.cpp | 2 +- 7 files changed, 20 insertions(+), 12 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ae4cbcf8..99e3d4c5 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -644,7 +644,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("MCLaunchMethod", "LauncherPart"); // Minecraft mods - m_settings->registerSetting("DontUseModMetadata", false); + m_settings->registerSetting("ModMetadataDisabled", false); // Minecraft offline player name m_settings->registerSetting("LastOfflinePlayerName", ""); diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index addb0dd8..fe807a29 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -10,7 +10,7 @@ ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir) void ModFolderLoadTask::run() { - if (!APPLICATION->settings()->get("DontUseModMetadata").toBool()) { + if (!APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { // Read metadata first getFromMetadata(); } @@ -34,14 +34,9 @@ void ModFolderLoadTask::run() void ModFolderLoadTask::getFromMetadata() { m_index_dir.refresh(); - for (auto entry : m_index_dir.entryList()) { - // QDir::Filter::NoDotAndDotDot seems to exclude all files for some reason... - if (entry == "." || entry == "..") - continue; - + for (auto entry : m_index_dir.entryList(QDir::Files)) { auto metadata = Metadata::get(m_index_dir, entry); - // TODO: Don't simply return. Instead, show to the user that the metadata is there, but - // it's not currently 'installed' (i.e. there's no JAR file yet). + if(!metadata.isValid()){ return; } diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index bb5c7c9d..2137f616 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -8,7 +8,7 @@ namespace ModPlatform { -enum class Provider{ +enum class Provider { MODRINTH, FLAME }; @@ -33,6 +33,7 @@ struct IndexedVersion { QString date; QString fileName; QVector loaders = {}; + QString hash; }; struct IndexedPack { diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 45f02b71..4b172c13 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -6,6 +6,8 @@ #include "modplatform/flame/FlameAPI.h" #include "net/NetJob.h" +static ModPlatform::ProviderCapabilities ProviderCaps; + void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); @@ -60,6 +62,12 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, file.downloadUrl = Json::requireString(obj, "downloadUrl"); file.fileName = Json::requireString(obj, "fileName"); + auto hash_list = Json::ensureArray(obj, "hashes"); + if(!hash_list.isEmpty()){ + if(hash_list.contains(ProviderCaps.hashType(ModPlatform::Provider::FLAME))) + file.hash = Json::requireString(hash_list, "value"); + } + unsortedVersions.append(file); } diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 6c8659dc..8b750740 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -24,6 +24,7 @@ #include "net/NetJob.h" static ModrinthAPI api; +static ModPlatform::ProviderCapabilities ProviderCaps; void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { @@ -95,6 +96,9 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, if (parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); file.fileName = Json::requireString(parent, "filename"); + auto hash_list = Json::requireObject(parent, "hashes"); + if(hash_list.contains(ProviderCaps.hashType(ModPlatform::Provider::MODRINTH))) + file.hash = Json::requireString(hash_list, ProviderCaps.hashType(ModPlatform::Provider::MODRINTH)); unsortedVersions.append(file); } diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 50f87c24..70efc6bd 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -30,7 +30,7 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo mod.url = mod_version.downloadUrl; mod.hash_format = ProviderCaps.hashType(mod_pack.provider); - mod.hash = ""; // FIXME + mod.hash = mod_version.hash; mod.provider = mod_pack.provider; mod.file_id = mod_pack.addonId; diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 8754c0ec..faf9272d 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -345,7 +345,7 @@ void LauncherPage::applySettings() } // Mods - s->set("DontUseModMetadata", ui->metadataDisableBtn->isChecked()); + s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked()); } void LauncherPage::loadSettings() { From 67e0214fa5c1ff36d3718c3fb68107bf0dfe7e5d Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 21 Apr 2022 15:47:46 -0300 Subject: [PATCH 485/605] fix: don't try to delete mods multiple times Shows a more helpful message if there's a parsing error when reading the index file. Also fixes a clazy warning with using the `.data()` method in a temporary QByteArray object. --- launcher/minecraft/mod/ModFolderModel.cpp | 3 +++ launcher/modplatform/packwiz/Packwiz.cpp | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index e034e35e..b2d8f03e 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -339,6 +339,9 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes) for (auto i: indexes) { + if(i.column() != 0) { + continue; + } Mod &m = mods[i.row()]; auto index_dir = indexDir(); m.destroy(index_dir); diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 70efc6bd..4fe4398a 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -162,12 +162,14 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod // NOLINTNEXTLINE(modernize-avoid-c-arrays) char errbuf[200]; - table = toml_parse(index_file.readAll().data(), errbuf, sizeof(errbuf)); + auto file_bytearray = index_file.readAll(); + table = toml_parse(file_bytearray.data(), errbuf, sizeof(errbuf)); index_file.close(); if (!table) { - qCritical() << QString("Could not open file %1!").arg(indexFileName(mod.name)); + qWarning() << QString("Could not open file %1!").arg(indexFileName(index_file_name)); + qWarning() << "Reason: " << QString(errbuf); return {}; } From 5c5699bba5ed2a5befb7c3f8d9fbcd679a8698ab Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 22 Apr 2022 13:23:47 -0300 Subject: [PATCH 486/605] refactor: move individual pack version parsing to its own function --- .../modrinth/ModrinthPackIndex.cpp | 95 +++++++++++-------- .../modplatform/modrinth/ModrinthPackIndex.h | 1 + 2 files changed, 55 insertions(+), 41 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 8b750740..aa798381 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -59,49 +59,10 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, for (auto versionIter : arr) { auto obj = versionIter.toObject(); - ModPlatform::IndexedVersion file; - file.addonId = Json::requireString(obj, "project_id"); - file.fileId = Json::requireString(obj, "id"); - file.date = Json::requireString(obj, "date_published"); - auto versionArray = Json::requireArray(obj, "game_versions"); - if (versionArray.empty()) { continue; } - for (auto mcVer : versionArray) { - file.mcVersion.append(mcVer.toString()); - } - auto loaders = Json::requireArray(obj, "loaders"); - for (auto loader : loaders) { - file.loaders.append(loader.toString()); - } - file.version = Json::requireString(obj, "name"); - - auto files = Json::requireArray(obj, "files"); - int i = 0; - - // Find correct file (needed in cases where one version may have multiple files) - // Will default to the last one if there's no primary (though I think Modrinth requires that - // at least one file is primary, idk) - // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed - while (i < files.count() - 1){ - auto parent = files[i].toObject(); - auto fileName = Json::requireString(parent, "filename"); - - // Grab the primary file, if available - if(Json::requireBoolean(parent, "primary")) - break; - - i++; - } - - auto parent = files[i].toObject(); - if (parent.contains("url")) { - file.downloadUrl = Json::requireString(parent, "url"); - file.fileName = Json::requireString(parent, "filename"); - auto hash_list = Json::requireObject(parent, "hashes"); - if(hash_list.contains(ProviderCaps.hashType(ModPlatform::Provider::MODRINTH))) - file.hash = Json::requireString(hash_list, ProviderCaps.hashType(ModPlatform::Provider::MODRINTH)); + 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 @@ -111,3 +72,55 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, pack.versions = unsortedVersions; pack.versionsLoaded = true; } + +auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedVersion +{ + ModPlatform::IndexedVersion file; + + file.addonId = Json::requireString(obj, "project_id"); + file.fileId = Json::requireString(obj, "id"); + file.date = Json::requireString(obj, "date_published"); + auto versionArray = Json::requireArray(obj, "game_versions"); + if (versionArray.empty()) { + return {}; + } + for (auto mcVer : versionArray) { + file.mcVersion.append(mcVer.toString()); + } + auto loaders = Json::requireArray(obj, "loaders"); + for (auto loader : loaders) { + file.loaders.append(loader.toString()); + } + file.version = Json::requireString(obj, "name"); + + auto files = Json::requireArray(obj, "files"); + int i = 0; + + // Find correct file (needed in cases where one version may have multiple files) + // Will default to the last one if there's no primary (though I think Modrinth requires that + // at least one file is primary, idk) + // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed + while (i < files.count() - 1) { + auto parent = files[i].toObject(); + auto fileName = Json::requireString(parent, "filename"); + + // Grab the primary file, if available + if (Json::requireBoolean(parent, "primary")) + break; + + i++; + } + + auto parent = files[i].toObject(); + if (parent.contains("url")) { + file.downloadUrl = Json::requireString(parent, "url"); + file.fileName = Json::requireString(parent, "filename"); + auto hash_list = Json::requireObject(parent, "hashes"); + if (hash_list.contains(ProviderCaps.hashType(ModPlatform::Provider::MODRINTH))) + file.hash = Json::requireString(hash_list, ProviderCaps.hashType(ModPlatform::Provider::MODRINTH)); + + return file; + } + + return {}; +} diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index 7f306f25..df70278f 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -29,5 +29,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr& network, BaseInstance* inst); +auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; } // namespace Modrinth From 59d628208b403bfb2442291cbca139cbdfcd325f Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 6 May 2022 12:42:01 -0300 Subject: [PATCH 487/605] feat: allow trying to use multiple hash types --- launcher/modplatform/ModIndex.cpp | 60 +++++++++++++++---- launcher/modplatform/ModIndex.h | 5 +- launcher/modplatform/flame/FlameModIndex.cpp | 10 +++- .../modrinth/ModrinthPackIndex.cpp | 10 +++- launcher/modplatform/packwiz/Packwiz.cpp | 2 +- 5 files changed, 68 insertions(+), 19 deletions(-) diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index b3c057fb..f6e134e0 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -1,26 +1,60 @@ #include "modplatform/ModIndex.h" -namespace ModPlatform{ +#include + +namespace ModPlatform { auto ProviderCapabilities::name(Provider p) -> const char* { - switch(p){ - case Provider::MODRINTH: - return "modrinth"; - case Provider::FLAME: - return "curseforge"; + switch (p) { + case Provider::MODRINTH: + return "modrinth"; + case Provider::FLAME: + return "curseforge"; } return {}; } -auto ProviderCapabilities::hashType(Provider p) -> QString +auto ProviderCapabilities::readableName(Provider p) -> QString { - switch(p){ - case Provider::MODRINTH: - return "sha512"; - case Provider::FLAME: - return "murmur2"; + switch (p) { + case Provider::MODRINTH: + return "Modrinth"; + case Provider::FLAME: + return "CurseForge"; + } + return {}; +} +auto ProviderCapabilities::hashType(Provider p) -> QStringList +{ + switch (p) { + case Provider::MODRINTH: + return { "sha512", "sha1" }; + case Provider::FLAME: + return { "murmur2" }; + } + return {}; +} +auto ProviderCapabilities::hash(Provider p, QByteArray& data, QString type) -> QByteArray +{ + switch (p) { + case Provider::MODRINTH: { + // NOTE: Data is the result of reading the entire JAR file! + + // If 'type' was specified, we use that + if (!type.isEmpty() && hashType(p).contains(type)) { + if (type == "sha512") + return QCryptographicHash::hash(data, QCryptographicHash::Sha512); + else if (type == "sha1") + return QCryptographicHash::hash(data, QCryptographicHash::Sha1); + } + + return QCryptographicHash::hash(data, QCryptographicHash::Sha512); + } + case Provider::FLAME: + // TODO + break; } return {}; } -} // namespace ModPlatform +} // namespace ModPlatform diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 2137f616..8ada1fc6 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -16,7 +16,9 @@ enum class Provider { class ProviderCapabilities { public: auto name(Provider) -> const char*; - auto hashType(Provider) -> QString; + auto readableName(Provider) -> QString; + auto hashType(Provider) -> QStringList; + auto hash(Provider, QByteArray&, QString type = "") -> QByteArray; }; struct ModpackAuthor { @@ -33,6 +35,7 @@ struct IndexedVersion { QString date; QString fileName; QVector loaders = {}; + QString hash_type; QString hash; }; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 4b172c13..63411275 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -64,8 +64,14 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, auto hash_list = Json::ensureArray(obj, "hashes"); if(!hash_list.isEmpty()){ - if(hash_list.contains(ProviderCaps.hashType(ModPlatform::Provider::FLAME))) - file.hash = Json::requireString(hash_list, "value"); + auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::FLAME); + for(auto& hash_type : hash_types) { + if(hash_list.contains(hash_type)) { + file.hash = Json::requireString(hash_list, "value"); + file.hash_type = hash_type; + break; + } + } } unsortedVersions.append(file); diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index aa798381..30693a82 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -116,8 +116,14 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV file.downloadUrl = Json::requireString(parent, "url"); file.fileName = Json::requireString(parent, "filename"); auto hash_list = Json::requireObject(parent, "hashes"); - if (hash_list.contains(ProviderCaps.hashType(ModPlatform::Provider::MODRINTH))) - file.hash = Json::requireString(hash_list, ProviderCaps.hashType(ModPlatform::Provider::MODRINTH)); + auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH); + for (auto& hash_type : hash_types) { + if (hash_list.contains(hash_type)) { + file.hash = Json::requireString(hash_list, hash_type); + file.hash_type = hash_type; + break; + } + } return file; } diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 4fe4398a..cb430c1f 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -29,7 +29,7 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo mod.filename = mod_version.fileName; mod.url = mod_version.downloadUrl; - mod.hash_format = ProviderCaps.hashType(mod_pack.provider); + mod.hash_format = mod_version.hash_type; mod.hash = mod_version.hash; mod.provider = mod_pack.provider; From 0985adfd74758891c2e61c2de7f930119cab1386 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 7 May 2022 19:39:00 -0300 Subject: [PATCH 488/605] change: support newest changes with packwiz regarding CF --- launcher/modplatform/ModIndex.cpp | 12 +++++++-- launcher/modplatform/flame/FlameModIndex.cpp | 25 +++++++++++++----- launcher/modplatform/packwiz/Packwiz.cpp | 15 ++++++++--- launcher/modplatform/packwiz/Packwiz.h | 6 ++--- launcher/modplatform/packwiz/Packwiz_test.cpp | 6 ++--- ...-mining.toml => borderless-mining.pw.toml} | Bin ...=> screenshot-to-clipboard-fabric.pw.toml} | Bin 7 files changed, 46 insertions(+), 18 deletions(-) rename launcher/modplatform/packwiz/testdata/{borderless-mining.toml => borderless-mining.pw.toml} (100%) rename launcher/modplatform/packwiz/testdata/{screenshot-to-clipboard-fabric.toml => screenshot-to-clipboard-fabric.pw.toml} (100%) diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index f6e134e0..6027c4f3 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -30,7 +30,8 @@ auto ProviderCapabilities::hashType(Provider p) -> QStringList case Provider::MODRINTH: return { "sha512", "sha1" }; case Provider::FLAME: - return { "murmur2" }; + // Try newer formats first, fall back to old format + return { "sha1", "md5", "murmur2" }; } return {}; } @@ -51,7 +52,14 @@ auto ProviderCapabilities::hash(Provider p, QByteArray& data, QString type) -> Q return QCryptographicHash::hash(data, QCryptographicHash::Sha512); } case Provider::FLAME: - // TODO + // If 'type' was specified, we use that + if (!type.isEmpty() && hashType(p).contains(type)) { + if(type == "sha1") + return QCryptographicHash::hash(data, QCryptographicHash::Sha1); + else if (type == "md5") + return QCryptographicHash::hash(data, QCryptographicHash::Md5); + } + break; } return {}; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 63411275..00dac411 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -30,6 +30,17 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) } } +static QString enumToString(int hash_algorithm) +{ + switch(hash_algorithm){ + default: + case 1: + return "sha1"; + case 2: + return "md5"; + } +} + void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr& network, @@ -63,14 +74,14 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, file.fileName = Json::requireString(obj, "fileName"); auto hash_list = Json::ensureArray(obj, "hashes"); - if(!hash_list.isEmpty()){ + for(auto h : hash_list){ + auto hash_entry = Json::ensureObject(h); auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::FLAME); - for(auto& hash_type : hash_types) { - if(hash_list.contains(hash_type)) { - file.hash = Json::requireString(hash_list, "value"); - file.hash_type = hash_type; - break; - } + auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm")); + if(hash_types.contains(hash_algo)){ + file.hash = Json::requireString(hash_entry, "value"); + file.hash_type = hash_algo; + break; } } diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index cb430c1f..1ad6d75b 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -14,9 +14,9 @@ namespace Packwiz { // Helpers static inline auto indexFileName(QString const& mod_name) -> QString { - if(mod_name.endsWith(".toml")) + if(mod_name.endsWith(".pw.toml")) return mod_name; - return QString("%1.toml").arg(mod_name); + return QString("%1.pw.toml").arg(mod_name); } static ModPlatform::ProviderCapabilities ProviderCaps; @@ -28,7 +28,14 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo mod.name = mod_pack.name; mod.filename = mod_version.fileName; - mod.url = mod_version.downloadUrl; + if(mod_pack.provider == ModPlatform::Provider::FLAME){ + mod.mode = "metadata:curseforge"; + } + else { + mod.mode = "url"; + mod.url = mod_version.downloadUrl; + } + mod.hash_format = mod_version.hash_type; mod.hash = mod_version.hash; @@ -84,6 +91,7 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) addToStream("side", mod.side); in_stream << QString("\n[download]\n"); + addToStream("mode", mod.mode); addToStream("url", mod.url.toString()); addToStream("hash-format", mod.hash_format); addToStream("hash", mod.hash); @@ -186,6 +194,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod return {}; } + mod.mode = stringEntry(download_table, "mode"); mod.url = stringEntry(download_table, "url"); mod.hash_format = stringEntry(download_table, "hash-format"); mod.hash = stringEntry(download_table, "hash"); diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 69125dbc..e66d0030 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -22,8 +22,8 @@ class V1 { QString side {"both"}; // [download] + QString mode {}; QUrl url {}; - // FIXME: make hash-format an enum QString hash_format {}; QString hash {}; @@ -42,11 +42,11 @@ class V1 { auto version() -> QVariant& { return file_id; } }; - /* Generates the object representing the information in a mod.toml file via + /* 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.toml file via + /* Generates the object representing the information in a mod.pw.toml file via * its common representation in the launcher. * */ static auto createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod; diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/launcher/modplatform/packwiz/Packwiz_test.cpp index 08de332d..9f3c486e 100644 --- a/launcher/modplatform/packwiz/Packwiz_test.cpp +++ b/launcher/modplatform/packwiz/Packwiz_test.cpp @@ -14,7 +14,7 @@ class PackwizTest : public QObject { QString source = QFINDTESTDATA("testdata"); QDir index_dir(source); - QString name_mod("borderless-mining.toml"); + QString name_mod("borderless-mining.pw.toml"); QVERIFY(index_dir.entryList().contains(name_mod)); auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod); @@ -39,10 +39,10 @@ class PackwizTest : public QObject { QString source = QFINDTESTDATA("testdata"); QDir index_dir(source); - QString name_mod("screenshot-to-clipboard-fabric.toml"); + QString name_mod("screenshot-to-clipboard-fabric.pw.toml"); QVERIFY(index_dir.entryList().contains(name_mod)); - // Try without the .toml at the end + // Try without the .pw.toml at the end name_mod.chop(5); auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod); diff --git a/launcher/modplatform/packwiz/testdata/borderless-mining.toml b/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml similarity index 100% rename from launcher/modplatform/packwiz/testdata/borderless-mining.toml rename to launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml diff --git a/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.toml b/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml similarity index 100% rename from launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.toml rename to launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml From 3a923060ceee142987e585d7ab4d78642f3506da Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 7 May 2022 10:27:01 -0300 Subject: [PATCH 489/605] fix: use correct hash_type when creating metadata also fix: wrong parameter name in LocalModUpdateTask's constructor also fix: correct hash_format in CF --- launcher/minecraft/mod/tasks/LocalModUpdateTask.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h index 15591b21..f21c0b06 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h @@ -10,7 +10,7 @@ class LocalModUpdateTask : public Task { public: using Ptr = shared_qobject_ptr; - explicit LocalModUpdateTask(QDir mods_dir, ModPlatform::IndexedPack& mod, ModPlatform::IndexedVersion& mod_version); + explicit LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack& mod, ModPlatform::IndexedVersion& mod_version); auto canAbort() const -> bool override { return true; } auto abort() -> bool override; From 2fc1b999117ceebc3ebf05d96b0a749e3a0f9e98 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 10 May 2022 19:57:47 -0300 Subject: [PATCH 490/605] chore: add license headers Prevents a massive inload of Scrumplex ditto's :) I didn't add it to every file modified in this PR because the other changes are pretty minor, and would explode the diff of the PR. I hope that's not a problem O_O --- launcher/ModDownloadTask.cpp | 20 +++++++- launcher/ModDownloadTask.h | 27 +++++++++-- launcher/minecraft/mod/MetadataHandler.h | 18 +++++++ launcher/minecraft/mod/Mod.cpp | 48 +++++++++++++------ launcher/minecraft/mod/Mod.h | 48 +++++++++++++------ launcher/minecraft/mod/ModDetails.h | 35 ++++++++++++++ .../mod/tasks/LocalModUpdateTask.cpp | 20 +++++++- .../minecraft/mod/tasks/LocalModUpdateTask.h | 18 +++++++ .../minecraft/mod/tasks/ModFolderLoadTask.cpp | 36 +++++++++++++- .../minecraft/mod/tasks/ModFolderLoadTask.h | 35 ++++++++++++++ launcher/modplatform/ModIndex.cpp | 18 +++++++ launcher/modplatform/ModIndex.h | 18 +++++++ .../modrinth/ModrinthPackIndex.cpp | 29 +++++------ launcher/modplatform/packwiz/Packwiz.cpp | 18 +++++++ launcher/modplatform/packwiz/Packwiz.h | 18 +++++++ launcher/modplatform/packwiz/Packwiz_test.cpp | 18 +++++++ 16 files changed, 373 insertions(+), 51 deletions(-) diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp index 52de9c94..301b6637 100644 --- a/launcher/ModDownloadTask.cpp +++ b/launcher/ModDownloadTask.cpp @@ -1,7 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 "ModDownloadTask.h" #include "Application.h" -#include "minecraft/mod/tasks/LocalModUpdateTask.h" +#include "minecraft/mod/ModFolderModel.h" ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr mods) : m_mod(mod), m_mod_version(version), mods(mods) diff --git a/launcher/ModDownloadTask.h b/launcher/ModDownloadTask.h index 5eaee187..f4438a8d 100644 --- a/launcher/ModDownloadTask.h +++ b/launcher/ModDownloadTask.h @@ -1,14 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 "QObjectPtr.h" -#include "minecraft/mod/ModFolderModel.h" -#include "modplatform/ModIndex.h" #include "net/NetJob.h" - #include "tasks/SequentialTask.h" + +#include "modplatform/ModIndex.h" #include "minecraft/mod/tasks/LocalModUpdateTask.h" +class ModFolderModel; + class ModDownloadTask : public SequentialTask { Q_OBJECT public: diff --git a/launcher/minecraft/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h index 26b1f799..56962818 100644 --- a/launcher/minecraft/mod/MetadataHandler.h +++ b/launcher/minecraft/mod/MetadataHandler.h @@ -1,3 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 261ae9d2..71a32d32 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -1,17 +1,37 @@ -/* 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. - */ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 +* +* 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 "Mod.h" diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 58c7a80f..96d471b4 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -1,17 +1,37 @@ -/* 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. - */ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 +* +* 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 diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index 75ffea32..3e0a7ab0 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 +* +* 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 diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index 47207ada..cbe16567 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -1,6 +1,22 @@ -#include "LocalModUpdateTask.h" +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 +#include "LocalModUpdateTask.h" #include "Application.h" #include "FileSystem.h" diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h index f21c0b06..2db183e0 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h @@ -1,3 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index fe807a29..62d856f6 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -1,5 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 +* +* 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 "ModFolderLoadTask.h" -#include #include "Application.h" #include "minecraft/mod/MetadataHandler.h" diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h index ba997874..89a0f84e 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 +* +* 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 diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index 6027c4f3..3c4b7887 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -1,3 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 "modplatform/ModIndex.h" #include diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 8ada1fc6..04dd2dac 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -1,3 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 30693a82..fdce71c3 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -1,19 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - 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 . - */ +* PolyMC - 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 "ModrinthPackIndex.h" #include "ModrinthAPI.h" diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 1ad6d75b..91a5f9c6 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -1,3 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 "Packwiz.h" #include diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index e66d0030..58b86484 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -1,3 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 "modplatform/ModIndex.h" diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/launcher/modplatform/packwiz/Packwiz_test.cpp index 9f3c486e..023b990e 100644 --- a/launcher/modplatform/packwiz/Packwiz_test.cpp +++ b/launcher/modplatform/packwiz/Packwiz_test.cpp @@ -1,3 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - 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 #include From 42f8ec5b1489c2073adf9d3526080c434dbddd90 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 13 May 2022 11:42:08 -0300 Subject: [PATCH 491/605] fix: do modrinth changes on flame too Also fix a dumb moment --- launcher/modplatform/flame/FlameModIndex.cpp | 75 +++++++++++--------- launcher/modplatform/flame/FlameModIndex.h | 1 + launcher/modplatform/packwiz/Packwiz.cpp | 4 +- 3 files changed, 45 insertions(+), 35 deletions(-) diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 00dac411..ed6d64c3 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -52,40 +52,13 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, for (auto versionIter : arr) { auto obj = versionIter.toObject(); + + auto file = loadIndexedPackVersion(obj); + if(!file.addonId.isValid()) + file.addonId = pack.addonId; - auto versionArray = Json::requireArray(obj, "gameVersions"); - if (versionArray.isEmpty()) { - continue; - } - - ModPlatform::IndexedVersion file; - for (auto mcVer : versionArray) { - auto str = mcVer.toString(); - - if (str.contains('.')) - file.mcVersion.append(str); - } - - file.addonId = pack.addonId; - file.fileId = Json::requireInteger(obj, "id"); - file.date = Json::requireString(obj, "fileDate"); - file.version = Json::requireString(obj, "displayName"); - file.downloadUrl = Json::requireString(obj, "downloadUrl"); - file.fileName = Json::requireString(obj, "fileName"); - - auto hash_list = Json::ensureArray(obj, "hashes"); - for(auto h : hash_list){ - auto hash_entry = Json::ensureObject(h); - auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::FLAME); - auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm")); - if(hash_types.contains(hash_algo)){ - file.hash = Json::requireString(hash_entry, "value"); - file.hash_type = hash_algo; - break; - } - } - - unsortedVersions.append(file); + 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 { @@ -96,3 +69,39 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, pack.versions = unsortedVersions; pack.versionsLoaded = true; } + +auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion +{ + auto versionArray = Json::requireArray(obj, "gameVersions"); + if (versionArray.isEmpty()) { + return {}; + } + + ModPlatform::IndexedVersion file; + for (auto mcVer : versionArray) { + auto str = mcVer.toString(); + + if (str.contains('.')) + file.mcVersion.append(str); + } + + file.addonId = Json::requireInteger(obj, "modId"); + file.fileId = Json::requireInteger(obj, "id"); + file.date = Json::requireString(obj, "fileDate"); + file.version = Json::requireString(obj, "displayName"); + file.downloadUrl = Json::requireString(obj, "downloadUrl"); + file.fileName = Json::requireString(obj, "fileName"); + + auto hash_list = Json::ensureArray(obj, "hashes"); + for (auto h : hash_list) { + auto hash_entry = Json::ensureObject(h); + auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::FLAME); + auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm")); + if (hash_types.contains(hash_algo)) { + file.hash = Json::requireString(hash_entry, "value"); + file.hash_type = hash_algo; + break; + } + } + return file; +} diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index d3171d94..2e0f2e86 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -16,5 +16,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr& network, BaseInstance* inst); +auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; } // namespace FlameMod diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 91a5f9c6..ee82f8a0 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -58,8 +58,8 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo mod.hash = mod_version.hash; mod.provider = mod_pack.provider; - mod.file_id = mod_pack.addonId; - mod.project_id = mod_version.fileId; + mod.file_id = mod_version.fileId; + mod.project_id = mod_pack.addonId; return mod; } From 5a1de15332bcfbeafff7d0c678d7286ca85cfe18 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 18 May 2022 05:46:07 -0300 Subject: [PATCH 492/605] fix: use a more robust method of finding metadata indexes Often times, mods can have their name in different forms, changing one letter to caps or the other way (e.g. JourneyMaps -> Journeymaps). This makes it possible to find those as well, which is not perfect by any means, but should suffice for the majority of cases. --- launcher/modplatform/packwiz/Packwiz.cpp | 110 +++++++++++++++-------- launcher/modplatform/packwiz/Packwiz.h | 6 ++ 2 files changed, 80 insertions(+), 36 deletions(-) diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index ee82f8a0..0782b9f4 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -23,12 +23,37 @@ #include #include "toml.h" +#include "FileSystem.h" #include "minecraft/mod/Mod.h" #include "modplatform/ModIndex.h" namespace Packwiz { +auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString +{ + QFile index_file(index_dir.absoluteFilePath(normalized_fname)); + + QString real_fname = normalized_fname; + if (!index_file.exists()) { + // Tries to get similar entries + for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { + if (!QString::compare(normalized_fname, file_name, Qt::CaseInsensitive)) { + real_fname = file_name; + break; + } + } + + if(should_find_match && !QString::compare(normalized_fname, real_fname, Qt::CaseSensitive)){ + qCritical() << "Could not find a match for a valid metadata file!"; + qCritical() << "File: " << normalized_fname; + return {}; + } + } + + return real_fname; +} + // Helpers static inline auto indexFileName(QString const& mod_name) -> QString { @@ -39,6 +64,33 @@ static inline auto indexFileName(QString const& mod_name) -> QString static ModPlatform::ProviderCapabilities ProviderCaps; +// Helper functions for extracting data from the TOML file +auto stringEntry(toml_table_t* parent, const char* entry_name) -> QString +{ + toml_datum_t var = toml_string_in(parent, entry_name); + if (!var.ok) { + qCritical() << QString("Failed to read str property '%1' in mod metadata.").arg(entry_name); + return {}; + } + + QString tmp = var.u.s; + free(var.u.s); + + return tmp; +} + +auto intEntry(toml_table_t* parent, const char* entry_name) -> int +{ + toml_datum_t var = toml_int_in(parent, entry_name); + if (!var.ok) { + qCritical() << QString("Failed to read int property '%1' in mod metadata.").arg(entry_name); + return {}; + } + + return var.u.i; +} + + auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod { Mod mod; @@ -86,7 +138,11 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) } // Ensure the corresponding mod's info exists, and create it if not - QFile index_file(index_dir.absoluteFilePath(indexFileName(mod.name))); + + auto normalized_fname = indexFileName(mod.name); + auto real_fname = getRealIndexName(index_dir, normalized_fname); + + QFile index_file(index_dir.absoluteFilePath(real_fname)); // There's already data on there! // TODO: We should do more stuff here, as the user is likely trying to @@ -127,11 +183,18 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) break; } } + + index_file.close(); } void V1::deleteModIndex(QDir& index_dir, QString& mod_name) { - QFile index_file(index_dir.absoluteFilePath(indexFileName(mod_name))); + auto normalized_fname = indexFileName(mod_name); + auto real_fname = getRealIndexName(index_dir, normalized_fname); + if (real_fname.isEmpty()) + return; + + QFile index_file(index_dir.absoluteFilePath(real_fname)); if(!index_file.exists()){ qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_name); @@ -143,42 +206,17 @@ void V1::deleteModIndex(QDir& index_dir, QString& mod_name) } } -// Helper functions for extracting data from the TOML file -static auto stringEntry(toml_table_t* parent, const char* entry_name) -> QString -{ - toml_datum_t var = toml_string_in(parent, entry_name); - if (!var.ok) { - qCritical() << QString("Failed to read str property '%1' in mod metadata.").arg(entry_name); - return {}; - } - - QString tmp = var.u.s; - free(var.u.s); - - return tmp; -} - -static auto intEntry(toml_table_t* parent, const char* entry_name) -> int -{ - toml_datum_t var = toml_int_in(parent, entry_name); - if (!var.ok) { - qCritical() << QString("Failed to read int property '%1' in mod metadata.").arg(entry_name); - return {}; - } - - return var.u.i; -} - auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod { Mod mod; - QFile index_file(index_dir.absoluteFilePath(indexFileName(index_file_name))); - - if (!index_file.exists()) { - qWarning() << QString("Tried to get a non-existent mod metadata for %1").arg(index_file_name); + auto normalized_fname = indexFileName(index_file_name); + auto real_fname = getRealIndexName(index_dir, normalized_fname, true); + if (real_fname.isEmpty()) return {}; - } + + QFile index_file(index_dir.absoluteFilePath(real_fname)); + if (!index_file.open(QIODevice::ReadOnly)) { qWarning() << QString("Failed to open mod metadata for %1").arg(index_file_name); return {}; @@ -198,14 +236,14 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod qWarning() << "Reason: " << QString(errbuf); return {}; } - - { // Basic info + + { // Basic info mod.name = stringEntry(table, "name"); mod.filename = stringEntry(table, "filename"); mod.side = stringEntry(table, "side"); } - { // [download] info + { // [download] info toml_table_t* download_table = toml_table_in(table, "download"); if (!download_table) { qCritical() << QString("No [download] section found on mod metadata!"); diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 58b86484..3c99769c 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -24,6 +24,7 @@ #include #include +struct toml_table_t; class QDir; // Mod from launcher/minecraft/mod/Mod.h @@ -31,6 +32,11 @@ class Mod; namespace Packwiz { +auto getRealIndexName(QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString; + +auto stringEntry(toml_table_t* parent, const char* entry_name) -> QString; +auto intEntry(toml_table_t* parent, const char* entry_name) -> int; + class V1 { public: struct Mod { From 997bf9144258c89f4c1e5fb23d7f3218790ac5ea Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 23 May 2022 14:15:49 -0400 Subject: [PATCH 493/605] Add desktop shortcut to Windows installer --- program_info/win_install.nsi | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi index 4ca4de1a..cb4c8d1d 100644 --- a/program_info/win_install.nsi +++ b/program_info/win_install.nsi @@ -141,12 +141,18 @@ Section "PolyMC" SectionEnd -Section "Start Menu Shortcuts" SHORTCUTS +Section "Start Menu Shortcut" SM_SHORTCUTS CreateShortcut "$SMPROGRAMS\PolyMC.lnk" "$INSTDIR\polymc.exe" "" "$INSTDIR\polymc.exe" 0 SectionEnd +Section "Desktop Shortcut" DESKTOP_SHORTCUTS + + CreateShortcut "$DESKTOP\PolyMC.lnk" "$INSTDIR\polymc.exe" "" "$INSTDIR\polymc.exe" 0 + +SectionEnd + ;-------------------------------- ; Uninstaller @@ -215,6 +221,7 @@ Section "Uninstall" RMDir /r $INSTDIR\styles Delete "$SMPROGRAMS\PolyMC.lnk" + Delete "$DESKTOP\PolyMC.lnk" RMDir "$INSTDIR" @@ -228,6 +235,7 @@ Function .onInit ${GetParameters} $R0 ${GetOptions} $R0 "/NoShortcuts" $R1 ${IfNot} ${Errors} - !insertmacro UnselectSection ${SHORTCUTS} + !insertmacro UnselectSection ${SM_SHORTCUTS} + !insertmacro UnselectSection ${DESKTOP_SHORTCUTS} ${EndIf} FunctionEnd From f28a0aa666565354e657dec59249aa1fd237cdb0 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Mon, 23 May 2022 19:42:04 +0100 Subject: [PATCH 494/605] ATLauncher: Handle main class depends --- .../atlauncher/ATLPackInstallTask.cpp | 20 ++++++++++++++++--- .../atlauncher/ATLPackManifest.cpp | 8 +++++++- .../modplatform/atlauncher/ATLPackManifest.h | 8 +++++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 9b14f355..e6fd1334 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -414,10 +414,24 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr profile) { - if(m_version.mainClass == QString() && m_version.extraArguments == QString()) { + if (m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.isEmpty()) { return true; } + auto mainClass = m_version.mainClass.mainClass; + + auto hasMainClassDepends = !m_version.mainClass.depends.isEmpty(); + if (hasMainClassDepends) { + QSet mods; + for (const auto& item : m_version.mods) { + mods.insert(item.name); + } + + if (hasMainClassDepends && !mods.contains(m_version.mainClass.depends)) { + mainClass = ""; + } + } + auto uuid = QUuid::createUuid(); auto id = uuid.toString().remove('{').remove('}'); auto target_id = "org.multimc.atlauncher." + id; @@ -442,8 +456,8 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< auto f = std::make_shared(); f->name = m_pack + " " + m_version_name; - if(m_version.mainClass != QString() && !mainClasses.contains(m_version.mainClass)) { - f->mainClass = m_version.mainClass; + if (!mainClass.isEmpty() && !mainClasses.contains(mainClass)) { + f->mainClass = mainClass; } // Parse out tweakers diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp index d01ec32c..cec9896b 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp +++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -212,6 +212,12 @@ static void loadVersionMessages(ATLauncher::VersionMessages& m, QJsonObject& obj m.update = Json::ensureString(obj, "update", ""); } +static void loadVersionMainClass(ATLauncher::PackVersionMainClass& m, QJsonObject& obj) +{ + m.mainClass = Json::ensureString(obj, "mainClass", ""); + m.depends = Json::ensureString(obj, "depends", ""); +} + void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) { v.version = Json::requireString(obj, "version"); @@ -220,7 +226,7 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) if(obj.contains("mainClass")) { auto main = Json::requireObject(obj, "mainClass"); - v.mainClass = Json::ensureString(main, "mainClass", ""); + loadVersionMainClass(v.mainClass, main); } if(obj.contains("extraArguments")) { diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h index 23e162e3..bf88d91d 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -150,12 +150,18 @@ struct VersionMessages QString update; }; +struct PackVersionMainClass +{ + QString mainClass; + QString depends; +}; + struct PackVersion { QString version; QString minecraft; bool noConfigs; - QString mainClass; + PackVersionMainClass mainClass; QString extraArguments; VersionLoader loader; From 101ca60b2bb1d3c3047bc5842461c68d05708e39 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Mon, 23 May 2022 20:14:23 +0100 Subject: [PATCH 495/605] ATLauncher: Handle extra arguments depends --- .../atlauncher/ATLPackInstallTask.cpp | 16 +++++++++++++--- .../modplatform/atlauncher/ATLPackManifest.cpp | 8 +++++++- .../modplatform/atlauncher/ATLPackManifest.h | 8 +++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index e6fd1334..b2dda4e4 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -414,14 +414,16 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr profile) { - if (m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.isEmpty()) { + if (m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.arguments.isEmpty()) { return true; } auto mainClass = m_version.mainClass.mainClass; + auto extraArguments = m_version.extraArguments.arguments; auto hasMainClassDepends = !m_version.mainClass.depends.isEmpty(); - if (hasMainClassDepends) { + auto hasExtraArgumentsDepends = !m_version.extraArguments.depends.isEmpty(); + if (hasMainClassDepends || hasExtraArgumentsDepends) { QSet mods; for (const auto& item : m_version.mods) { mods.insert(item.name); @@ -430,6 +432,14 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< if (hasMainClassDepends && !mods.contains(m_version.mainClass.depends)) { mainClass = ""; } + + if (hasExtraArgumentsDepends && !mods.contains(m_version.extraArguments.depends)) { + extraArguments = ""; + } + } + + if (mainClass.isEmpty() && extraArguments.isEmpty()) { + return true; } auto uuid = QUuid::createUuid(); @@ -461,7 +471,7 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< } // Parse out tweakers - auto args = m_version.extraArguments.split(" "); + auto args = extraArguments.split(" "); QString previous; for(auto arg : args) { if(arg.startsWith("--tweakClass=") || previous == "--tweakClass") { diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp index cec9896b..3af02a09 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp +++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -218,6 +218,12 @@ static void loadVersionMainClass(ATLauncher::PackVersionMainClass& m, QJsonObjec m.depends = Json::ensureString(obj, "depends", ""); } +static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a, QJsonObject& obj) +{ + a.arguments = Json::ensureString(obj, "arguments", ""); + a.depends = Json::ensureString(obj, "depends", ""); +} + void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) { v.version = Json::requireString(obj, "version"); @@ -231,7 +237,7 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) if(obj.contains("extraArguments")) { auto arguments = Json::requireObject(obj, "extraArguments"); - v.extraArguments = Json::ensureString(arguments, "arguments", ""); + loadVersionExtraArguments(v.extraArguments, arguments); } if(obj.contains("loader")) { diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h index bf88d91d..43510c50 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -156,13 +156,19 @@ struct PackVersionMainClass QString depends; }; +struct PackVersionExtraArguments +{ + QString arguments; + QString depends; +}; + struct PackVersion { QString version; QString minecraft; bool noConfigs; PackVersionMainClass mainClass; - QString extraArguments; + PackVersionExtraArguments extraArguments; VersionLoader loader; QVector libraries; From 4ee5264e24e21d89185d424072dc39cb6b2dd10f Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Mon, 23 May 2022 21:37:09 +0100 Subject: [PATCH 496/605] ATLauncher: Delete files from configs if they conflict with a mod --- .../modplatform/atlauncher/ATLPackInstallTask.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index b2dda4e4..62c7bf6d 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -781,6 +781,17 @@ bool PackInstallTask::extractMods( for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) { auto &from = iter.key(); auto &to = iter.value(); + + // If the file already exists, assume the mod is the correct copy - and remove + // the copy from the Configs.zip + QFileInfo fileInfo(to); + if (fileInfo.exists()) { + if (!QFile::remove(to)) { + qWarning() << "Failed to delete" << to; + return false; + } + } + FS::copy fileCopyOperation(from, to); if(!fileCopyOperation()) { qWarning() << "Failed to copy" << from << "to" << to; From fce5c575480c88f81f325f3759889d0cde9a28e0 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 23 May 2022 17:27:35 -0400 Subject: [PATCH 497/605] Silence CMake QuaZip not found warnings These are expected most of the time, and thus just noise. --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e2635c3f..fcc2512d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -143,7 +143,7 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 5) find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml) if(NOT Launcher_FORCE_BUNDLED_LIBS) - find_package(QuaZip-Qt5 1.3) + 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) From 0426149580feaca188c7f34b268411ffeb8787b0 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Tue, 24 May 2022 13:35:01 +0800 Subject: [PATCH 498/605] standard macOS app behavior --- launcher/Application.cpp | 24 ++++++++++++++++++++++++ launcher/Application.h | 7 +++++++ 2 files changed, 31 insertions(+) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ba4096b6..bcfdc460 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -871,6 +871,10 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_mcedit.reset(new MCEditTool(m_settings)); } + connect(this, &Application::clickedOnDock, [this]() { + this->showMainWindow(); + }); + connect(this, &Application::aboutToQuit, [this](){ if(m_instances) { @@ -954,6 +958,22 @@ bool Application::createSetupWizard() return false; } +bool Application::event(QEvent* event) { +#ifdef Q_OS_MACOS + if (event->type() == QEvent::ApplicationStateChange) { + auto ev = static_cast(event); + + if (m_prevAppState == Qt::ApplicationActive + && ev->applicationState() == Qt::ApplicationActive) { + qDebug() << "Clicked on dock!"; + emit clickedOnDock(); + } + m_prevAppState = ev->applicationState(); + } +#endif + return QApplication::event(event); +} + void Application::setupWizardFinished(int status) { qDebug() << "Wizard result =" << status; @@ -1284,6 +1304,10 @@ void Application::subRunningInstance() bool Application::shouldExitNow() const { +#ifdef Q_OS_MACOS + return false; +#endif + return m_runningInstances == 0 && m_openWindows == 0; } diff --git a/launcher/Application.h b/launcher/Application.h index 3129b4fb..d6a5473d 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -94,6 +94,8 @@ public: Application(int &argc, char **argv); virtual ~Application(); + bool event(QEvent* event) override; + std::shared_ptr settings() const { return m_settings; } @@ -180,6 +182,7 @@ signals: void updateAllowedChanged(bool status); void globalSettingsAboutToOpen(); void globalSettingsClosed(); + void clickedOnDock(); public slots: bool launch( @@ -238,6 +241,10 @@ private: QString m_rootPath; Status m_status = Application::StartingUp; +#ifdef Q_OS_MACOS + Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive; +#endif + #if defined Q_OS_WIN32 // used on Windows to attach the standard IO streams bool consoleAttached = false; From 9673dac22b0ff81a54847d5db5438c099a6df587 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Tue, 24 May 2022 16:18:02 +0800 Subject: [PATCH 499/605] add more `#ifdef`s --- launcher/Application.cpp | 2 ++ launcher/Application.h | 3 +++ 2 files changed, 5 insertions(+) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index bcfdc460..ff0f2129 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -871,9 +871,11 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_mcedit.reset(new MCEditTool(m_settings)); } +#ifdef Q_OS_MACOS connect(this, &Application::clickedOnDock, [this]() { this->showMainWindow(); }); +#endif connect(this, &Application::aboutToQuit, [this](){ if(m_instances) diff --git a/launcher/Application.h b/launcher/Application.h index d6a5473d..686137ec 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -182,7 +182,10 @@ signals: void updateAllowedChanged(bool status); void globalSettingsAboutToOpen(); void globalSettingsClosed(); + +#ifdef Q_OS_MACOS void clickedOnDock(); +#endif public slots: bool launch( From ca3c6c5e8a5151ea50e51f09938b894e6a610626 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 24 May 2022 09:38:48 -0300 Subject: [PATCH 500/605] feat: add donate links for modrinth mods --- launcher/modplatform/ModAPI.h | 2 + launcher/modplatform/ModIndex.h | 14 ++++ launcher/modplatform/flame/FlameAPI.h | 2 + .../modplatform/helpers/NetworkModAPI.cpp | 25 +++++++ launcher/modplatform/helpers/NetworkModAPI.h | 2 + launcher/modplatform/modrinth/ModrinthAPI.h | 5 ++ .../modrinth/ModrinthPackIndex.cpp | 21 ++++++ .../modplatform/modrinth/ModrinthPackIndex.h | 1 + launcher/ui/pages/modplatform/ModModel.cpp | 20 ++++++ launcher/ui/pages/modplatform/ModModel.h | 4 ++ launcher/ui/pages/modplatform/ModPage.cpp | 68 +++++++++++++------ launcher/ui/pages/modplatform/ModPage.h | 4 +- .../modplatform/modrinth/ModrinthModModel.cpp | 5 ++ .../modplatform/modrinth/ModrinthModModel.h | 1 + 14 files changed, 151 insertions(+), 23 deletions(-) diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index 4230df0b..24d80385 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -7,6 +7,7 @@ namespace ModPlatform { class ListModel; +struct IndexedPack; } class ModAPI { @@ -35,6 +36,7 @@ class ModAPI { }; virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0; + virtual void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) = 0; struct VersionSearchArgs { diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 7e1cf254..6e1a01bc 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -13,6 +13,12 @@ struct ModpackAuthor { QString url; }; +struct DonationData { + QString id; + QString platform; + QString url; +}; + struct IndexedVersion { QVariant addonId; QVariant fileId; @@ -24,6 +30,10 @@ struct IndexedVersion { QVector loaders = {}; }; +struct ExtraPackData { + QList donate; +}; + struct IndexedPack { QVariant addonId; QString name; @@ -35,6 +45,10 @@ struct IndexedPack { bool versionsLoaded = false; QVector versions; + + // Don't load by default, since some modplatform don't have that info + bool extraDataLoaded = true; + ExtraPackData extraData; }; } // namespace ModPlatform diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 8bb33d47..6ce474c8 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -41,6 +41,8 @@ class FlameAPI : public NetworkModAPI { .arg(gameVersionStr); }; + inline auto getModInfoURL(QString& id) const -> QString override { return {}; }; + inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { QString gameVersionQuery = args.mcVersions.size() == 1 ? QString("gameVersion=%1&").arg(args.mcVersions.front().toString()) : ""; diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp index 6829b837..d7abd10f 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.cpp +++ b/launcher/modplatform/helpers/NetworkModAPI.cpp @@ -31,6 +31,31 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const netJob->start(); } +void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) +{ + auto id_str = pack.addonId.toString(); + auto netJob = new NetJob(QString("%1::ModInfo").arg(id_str), APPLICATION->network()); + auto searchUrl = getModInfoURL(id_str); + + auto response = new QByteArray(); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + + QObject::connect(netJob, &NetJob::succeeded, [response, &pack, caller] { + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for " << pack.name << " at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + caller->infoRequestFinished(doc, pack); + }); + + netJob->start(); +} + void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) const { auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(caller->debugName()).arg(args.addonId), APPLICATION->network()); diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h index 000620b2..87d77ad1 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.h +++ b/launcher/modplatform/helpers/NetworkModAPI.h @@ -5,9 +5,11 @@ class NetworkModAPI : public ModAPI { public: void searchMods(CallerType* caller, SearchArgs&& args) const override; + void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) override; void getVersions(CallerType* caller, VersionSearchArgs&& args) const override; protected: virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0; + virtual auto getModInfoURL(QString& id) const -> QString = 0; virtual auto getVersionsURL(VersionSearchArgs& args) const -> QString = 0; }; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 79bc5175..13b62f0c 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -75,6 +75,11 @@ class ModrinthAPI : public NetworkModAPI { .arg(getGameVersionsArray(args.versions)); }; + inline auto getModInfoURL(QString& id) const -> QString override + { + return BuildConfig.MODRINTH_PROD_URL + "/project/" + id; + }; + inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { return QString(BuildConfig.MODRINTH_PROD_URL + diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index f7fa9864..32b4cfd4 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -45,6 +45,27 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) modAuthor.name = Json::requireString(obj, "author"); modAuthor.url = api.getAuthorURL(modAuthor.name); pack.authors.append(modAuthor); + + // Modrinth can have more data than what's provided by the basic search :) + pack.extraDataLoaded = false; +} + +void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj) +{ + auto donate_arr = Json::ensureArray(obj, "donation_urls"); + 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"); + + pack.extraData.donate.append(donate); + } + + pack.extraDataLoaded = true; } void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index 7f306f25..b0e3736f 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -25,6 +25,7 @@ namespace Modrinth { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr& network, diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 9dd8f737..13d9ceea 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -79,6 +79,11 @@ void ListModel::performPaginatedSearch() this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() }); } +void ListModel::requestModInfo(ModPlatform::IndexedPack& current) +{ + m_parent->apiProvider()->getModInfo(this, current); +} + void ListModel::refresh() { if (jobPtr) { @@ -225,6 +230,21 @@ void ListModel::searchRequestFailed(QString reason) } } +void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack) +{ + qDebug() << "Loading mod info"; + + try { + auto obj = Json::requireObject(doc); + loadExtraPackInfo(pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause(); + } + + m_parent->updateUi(); +} + void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) { auto& current = m_parent->getCurrent(); diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index d460cff2..dd22407c 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -36,9 +36,11 @@ class ListModel : public QAbstractListModel { void fetchMore(const QModelIndex& parent) override; void refresh(); void searchWithTerm(const QString& term, const int sort, const bool filter_changed); + void requestModInfo(ModPlatform::IndexedPack& current); void requestModVersions(const ModPlatform::IndexedPack& current); virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; + virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) {}; virtual void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) = 0; void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); @@ -49,6 +51,8 @@ class ListModel : public QAbstractListModel { void searchRequestFinished(QJsonDocument& doc); void searchRequestFailed(QString reason); + void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack); + void versionRequestSucceeded(QJsonDocument doc, QString addonId); protected slots: diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index ad36cf2f..4a02f5a4 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -94,28 +94,6 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) if (!first.isValid()) { return; } current = listModel->data(first, Qt::UserRole).value(); - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "" + name + ""; - - if (!current.authors.empty()) { - auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { - if (author.url.isEmpty()) { return author.name; } - return QString("%2").arg(author.url, author.name); - }; - QStringList authorStrs; - for (auto& author : current.authors) { - authorStrs.push_back(authorToStr(author)); - } - text += "
" + tr(" by ") + authorStrs.join(", "); - } - text += "

"; - - ui->packDescription->setHtml(text + current.description); if (!current.versionsLoaded) { qDebug() << QString("Loading %1 mod versions").arg(debugName()); @@ -132,6 +110,13 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) updateSelectionButton(); } + + if(!current.extraDataLoaded){ + qDebug() << QString("Loading %1 mod info").arg(debugName()); + listModel->requestModInfo(current); + } + + updateUi(); } void ModPage::onVersionSelectionChanged(QString data) @@ -207,3 +192,42 @@ void ModPage::updateSelectionButton() ui->modSelectionButton->setText(tr("Deselect mod for download")); } } + +void ModPage::updateUi() +{ + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name; + else + text = "" + name + ""; + + if (!current.authors.empty()) { + auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { + if (author.url.isEmpty()) { return author.name; } + return QString("%2").arg(author.url, author.name); + }; + QStringList authorStrs; + for (auto& author : current.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "
" + tr(" by ") + authorStrs.join(", "); + } + + if(!current.extraData.donate.isEmpty()) { + text += "

Donation information:
"; + auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { + return QString("%2").arg(donate.url, donate.platform); + }; + QStringList donates; + for (auto& donate : current.extraData.donate) { + donates.append(donateToStr(donate)); + } + text += donates.join(", "); + } + + text += "

"; + + ui->packDescription->setHtml(text + current.description); +} diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 0e658a8d..9522cc4c 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -36,10 +36,12 @@ class ModPage : public QWidget, public BasePage { void retranslate() override; + void updateUi(); + auto shouldDisplay() const -> bool override = 0; virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool = 0; - auto apiProvider() const -> const ModAPI* { return api.get(); }; + auto apiProvider() -> ModAPI* { return api.get(); }; auto getFilter() const -> const std::shared_ptr { return m_filter; } auto getCurrent() -> ModPlatform::IndexedPack& { return current; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp index 1d9f4d60..af92e63e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp @@ -30,6 +30,11 @@ void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) Modrinth::loadIndexedPack(m, obj); } +void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + Modrinth::loadExtraPackData(m, obj); +} + void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h index ae7b0bdd..386897fd 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h @@ -31,6 +31,7 @@ class ListModel : public ModPlatform::ListModel { private: 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; From 22e0527502683a625c5963ec8155e07d9ec06d28 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 24 May 2022 09:46:58 -0300 Subject: [PATCH 501/605] feat: add donate info to modrinth modpacks --- .../modplatform/modrinth/ModrinthPackManifest.cpp | 13 +++++++++++++ .../modplatform/modrinth/ModrinthPackManifest.h | 9 +++++++++ .../ui/pages/modplatform/modrinth/ModrinthPage.cpp | 12 ++++++++++++ 3 files changed, 34 insertions(+) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index f1ad39ce..f47942a0 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -65,6 +65,19 @@ void loadIndexedInfo(Modpack& pack, QJsonObject& obj) pack.extra.sourceUrl = Json::ensureString(obj, "source_url"); pack.extra.wikiUrl = Json::ensureString(obj, "wiki_url"); + 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.extraInfoLoaded = true; } diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index e5fc9a70..c8ca3660 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -58,12 +58,21 @@ struct File QUrl download; }; +struct DonationData { + QString id; + QString platform; + QString url; +}; + struct ModpackExtra { QString body; QString projectUrl; QString sourceUrl; QString wikiUrl; + + QList donate; + }; struct ModpackVersion { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 9bd24b57..f44d05f2 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -224,6 +224,18 @@ void ModrinthPage::updateUI() // TODO: Implement multiple authors with links text += "
" + tr(" by ") + QString("%2").arg(std::get<1>(current.author).toString(), std::get<0>(current.author)); + if(!current.extra.donate.isEmpty()) { + text += "

Donation information:
"; + auto donateToStr = [](Modrinth::DonationData& donate) -> QString { + return QString("%2").arg(donate.url, donate.platform); + }; + QStringList donates; + for (auto& donate : current.extra.donate) { + donates.append(donateToStr(donate)); + } + text += donates.join(", "); + } + text += "
"; HoeDown h; From 5e17d53c7f2e19b6911645d68e0e8a68b6e07d1d Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 24 May 2022 11:11:40 -0300 Subject: [PATCH 502/605] fix: missing tr() and update donate message --- launcher/ui/pages/modplatform/ModPage.cpp | 2 +- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 4a02f5a4..39e47edc 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -216,7 +216,7 @@ void ModPage::updateUi() } if(!current.extraData.donate.isEmpty()) { - text += "

Donation information:
"; + text += tr("

Donate information:
"); auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { return QString("%2").arg(donate.url, donate.platform); }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index f44d05f2..f7c5b2ce 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -225,7 +225,7 @@ void ModrinthPage::updateUI() text += "
" + tr(" by ") + QString("%2").arg(std::get<1>(current.author).toString(), std::get<0>(current.author)); if(!current.extra.donate.isEmpty()) { - text += "

Donation information:
"; + text += tr("

Donate information:
"); auto donateToStr = [](Modrinth::DonationData& donate) -> QString { return QString("%2").arg(donate.url, donate.platform); }; From 17b30b2ae25a6138f7d0452805998dfd24cd683e Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Tue, 24 May 2022 22:37:00 +0800 Subject: [PATCH 503/605] clean up .clang-format --- .clang-format | 220 +++----------------------------------------------- 1 file changed, 12 insertions(+), 208 deletions(-) diff --git a/.clang-format b/.clang-format index ed96c030..f90a4060 100644 --- a/.clang-format +++ b/.clang-format @@ -1,211 +1,15 @@ --- Language: Cpp -# BasedOnStyle: Chromium -AccessModifierOffset: -1 -AlignAfterOpenBracket: Align -AlignArrayOfStructures: None -AlignConsecutiveMacros: false # changed -AlignConsecutiveAssignments: false # changed -AlignConsecutiveBitFields: None -AlignConsecutiveDeclarations: None -AlignEscapedNewlines: Left -AlignOperands: Align -AlignTrailingComments: true -AllowAllArgumentsOnNextLine: true -AllowAllParametersOfDeclarationOnNextLine: false -AllowShortEnumsOnASingleLine: true -AllowShortBlocksOnASingleLine: false -AllowShortCaseLabelsOnASingleLine: false -AllowShortFunctionsOnASingleLine: Inline -AllowShortLambdasOnASingleLine: All -AllowShortIfStatementsOnASingleLine: false # changed -AllowShortLoopsOnASingleLine: false -AlwaysBreakAfterDefinitionReturnType: None -AlwaysBreakAfterReturnType: None -AlwaysBreakBeforeMultilineStrings: true -AlwaysBreakTemplateDeclarations: Yes -AttributeMacros: - - __capability -BinPackArguments: true -BinPackParameters: false +BasedOnStyle: Chromium +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: false +AllowShortIfStatementsOnASingleLine: false BraceWrapping: - AfterCaseLabel: false - AfterClass: false - AfterControlStatement: Never - AfterEnum: false - AfterFunction: true # changed - AfterNamespace: false - AfterObjCDeclaration: false - AfterStruct: false - AfterUnion: false - AfterExternBlock: false - BeforeCatch: false - BeforeElse: false - BeforeLambdaBody: false - BeforeWhile: false - IndentBraces: false - SplitEmptyFunction: false # changed - SplitEmptyRecord: false # changed - SplitEmptyNamespace: false # changed -BreakBeforeBinaryOperators: None -BreakBeforeConceptDeclarations: true -BreakBeforeBraces: Custom # changed -BreakBeforeInheritanceComma: false -BreakInheritanceList: BeforeColon -BreakBeforeTernaryOperators: true -BreakConstructorInitializersBeforeComma: true -BreakConstructorInitializers: BeforeComma # changed -BreakAfterJavaFieldAnnotations: false -BreakStringLiterals: true -ColumnLimit: 140 # changed -CommentPragmas: '^ IWYU pragma:' -CompactNamespaces: false -ConstructorInitializerAllOnOneLineOrOnePerLine: true -ConstructorInitializerIndentWidth: 4 -ContinuationIndentWidth: 4 -Cpp11BracedListStyle: false # changed -DeriveLineEnding: true -DerivePointerAlignment: false -DisableFormat: false -EmptyLineAfterAccessModifier: Never -EmptyLineBeforeAccessModifier: LogicalBlock -ExperimentalAutoDetectBinPacking: false -FixNamespaceComments: true -ForEachMacros: - - foreach - - Q_FOREACH - - BOOST_FOREACH -IfMacros: - - KJ_IF_MAYBE -IncludeBlocks: Preserve -IncludeCategories: - - Regex: '^' - Priority: 2 - SortPriority: 0 - CaseSensitive: false - - Regex: '^<.*\.h>' - Priority: 1 - SortPriority: 0 - CaseSensitive: false - - Regex: '^<.*' - Priority: 2 - SortPriority: 0 - CaseSensitive: false - - Regex: '.*' - Priority: 3 - SortPriority: 0 - CaseSensitive: false -IncludeIsMainRegex: '([-_](test|unittest))?$' -IncludeIsMainSourceRegex: '' -IndentAccessModifiers: false -IndentCaseLabels: true -IndentCaseBlocks: false -IndentGotoLabels: true -IndentPPDirectives: None -IndentExternBlock: AfterExternBlock -IndentRequires: false -IndentWidth: 4 # changed -IndentWrappedFunctionNames: false -InsertTrailingCommas: None -JavaScriptQuotes: Leave -JavaScriptWrapImports: true -KeepEmptyLinesAtTheStartOfBlocks: false -LambdaBodyIndentation: Signature -MacroBlockBegin: '' -MacroBlockEnd: '' -MaxEmptyLinesToKeep: 1 -NamespaceIndentation: None -ObjCBinPackProtocolList: Never -ObjCBlockIndentWidth: 2 -ObjCBreakBeforeNestedBlockParam: true -ObjCSpaceAfterProperty: false -ObjCSpaceBeforeProtocolList: true -PenaltyBreakAssignment: 2 -PenaltyBreakBeforeFirstCallParameter: 1 -PenaltyBreakComment: 300 -PenaltyBreakFirstLessLess: 120 -PenaltyBreakString: 1000 -PenaltyBreakTemplateDeclaration: 10 -PenaltyExcessCharacter: 1000000 -PenaltyReturnTypeOnItsOwnLine: 200 -PenaltyIndentedWhitespace: 0 -PointerAlignment: Left -PPIndentWidth: -1 -RawStringFormats: - - Language: Cpp - Delimiters: - - cc - - CC - - cpp - - Cpp - - CPP - - 'c++' - - 'C++' - CanonicalDelimiter: '' - BasedOnStyle: google - - Language: TextProto - Delimiters: - - pb - - PB - - proto - - PROTO - EnclosingFunctions: - - EqualsProto - - EquivToProto - - PARSE_PARTIAL_TEXT_PROTO - - PARSE_TEST_PROTO - - PARSE_TEXT_PROTO - - ParseTextOrDie - - ParseTextProtoOrDie - - ParseTestProto - - ParsePartialTestProto - CanonicalDelimiter: pb - BasedOnStyle: google -ReferenceAlignment: Pointer -ReflowComments: true -ShortNamespaceLines: 1 -SortIncludes: CaseSensitive -SortJavaStaticImport: Before -SortUsingDeclarations: true -SpaceAfterCStyleCast: false -SpaceAfterLogicalNot: false -SpaceAfterTemplateKeyword: true -SpaceBeforeAssignmentOperators: true -SpaceBeforeCaseColon: false -SpaceBeforeCpp11BracedList: false -SpaceBeforeCtorInitializerColon: true -SpaceBeforeInheritanceColon: true -SpaceBeforeParens: ControlStatements -SpaceAroundPointerQualifiers: Default -SpaceBeforeRangeBasedForLoopColon: true -SpaceInEmptyBlock: false -SpaceInEmptyParentheses: false -SpacesBeforeTrailingComments: 2 -SpacesInAngles: Never -SpacesInConditionalStatement: false -SpacesInContainerLiterals: true -SpacesInCStyleCastParentheses: false -SpacesInLineCommentPrefix: - Minimum: 1 - Maximum: -1 -SpacesInParentheses: false -SpacesInSquareBrackets: false -SpaceBeforeSquareBrackets: false -BitFieldColonSpacing: Both -Standard: Auto -StatementAttributeLikeMacros: - - Q_EMIT -StatementMacros: - - Q_UNUSED - - QT_REQUIRE_VERSION -TabWidth: 8 -UseCRLF: false -UseTab: Never -WhitespaceSensitiveMacros: - - STRINGIZE - - PP_STRINGIZE - - BOOST_PP_STRINGIZE - - NS_SWIFT_NAME - - CF_SWIFT_NAME -... - + AfterFunction: true + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeBraces: Custom +BreakConstructorInitializers: BeforeComma +ColumnLimit: 140 +Cpp11BracedListStyle: false From d0337da8ea54c272aadfe30bfe0474ae82011109 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 24 May 2022 11:52:27 -0300 Subject: [PATCH 504/605] feat: add remaining links to modrinth modpacks --- .../modrinth/ModrinthPackManifest.cpp | 14 +++++++ .../modrinth/ModrinthPackManifest.h | 3 ++ .../modplatform/modrinth/ModrinthPage.cpp | 38 ++++++++++++++----- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index f47942a0..73c8ef84 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -62,8 +62,22 @@ 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){ diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index c8ca3660..e95cb589 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -68,8 +68,11 @@ struct ModpackExtra { QString body; QString projectUrl; + + QString issuesUrl; QString sourceUrl; QString wikiUrl; + QString discordUrl; QList donate; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index f7c5b2ce..d8500674 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -224,19 +224,37 @@ void ModrinthPage::updateUI() // TODO: Implement multiple authors with links text += "
" + tr(" by ") + QString("%2").arg(std::get<1>(current.author).toString(), std::get<0>(current.author)); - if(!current.extra.donate.isEmpty()) { - text += tr("

Donate information:
"); - auto donateToStr = [](Modrinth::DonationData& donate) -> QString { - return QString("%2").arg(donate.url, donate.platform); - }; - QStringList donates; - for (auto& donate : current.extra.donate) { - donates.append(donateToStr(donate)); + if(current.extraInfoLoaded) { + if (!current.extra.donate.isEmpty()) { + text += "

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


"; HoeDown h; text += h.process(current.extra.body.toUtf8()); From ae2ef324f297adee33968b50e70d9cf5d8ed72fb Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 24 May 2022 11:58:11 -0300 Subject: [PATCH 505/605] feat: add remaining links to modrinth mods --- launcher/modplatform/ModIndex.h | 5 +++ .../modrinth/ModrinthPackIndex.cpp | 16 ++++++++ launcher/ui/pages/modplatform/ModPage.cpp | 39 ++++++++++++++----- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 6e1a01bc..4d1d02a5 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -32,6 +32,11 @@ struct IndexedVersion { struct ExtraPackData { QList donate; + + QString issuesUrl; + QString sourceUrl; + QString wikiUrl; + QString discordUrl; }; struct IndexedPack { diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 32b4cfd4..a9aa3a9d 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -52,6 +52,22 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj) { + pack.extraData.issuesUrl = Json::ensureString(obj, "issues_url"); + if(pack.extraData.issuesUrl.endsWith('/')) + pack.extraData.issuesUrl.chop(1); + + pack.extraData.sourceUrl = Json::ensureString(obj, "source_url"); + if(pack.extraData.sourceUrl.endsWith('/')) + pack.extraData.sourceUrl.chop(1); + + pack.extraData.wikiUrl = Json::ensureString(obj, "wiki_url"); + if(pack.extraData.wikiUrl.endsWith('/')) + pack.extraData.wikiUrl.chop(1); + + pack.extraData.discordUrl = Json::ensureString(obj, "discord_url"); + if(pack.extraData.discordUrl.endsWith('/')) + pack.extraData.discordUrl.chop(1); + auto donate_arr = Json::ensureArray(obj, "donation_urls"); for(auto d : donate_arr){ auto d_obj = Json::requireObject(d); diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 39e47edc..e0251160 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -215,19 +215,38 @@ void ModPage::updateUi() text += "
" + tr(" by ") + authorStrs.join(", "); } - if(!current.extraData.donate.isEmpty()) { - text += tr("

Donate information:
"); - auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { - return QString("%2").arg(donate.url, donate.platform); - }; - QStringList donates; - for (auto& donate : current.extraData.donate) { - donates.append(donateToStr(donate)); + + if(current.extraDataLoaded) { + if (!current.extraData.donate.isEmpty()) { + text += "

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

" + tr("External links:") + "
"; + } + + if (!current.extraData.issuesUrl.isEmpty()) + text += "- " + tr("Issues: %1").arg(current.extraData.issuesUrl) + "
"; + if (!current.extraData.wikiUrl.isEmpty()) + text += "- " + tr("Wiki: %1").arg(current.extraData.wikiUrl) + "
"; + if (!current.extraData.sourceUrl.isEmpty()) + text += "- " + tr("Source code: %1").arg(current.extraData.sourceUrl) + "
"; + if (!current.extraData.discordUrl.isEmpty()) + text += "- " + tr("Discord: %1").arg(current.extraData.discordUrl) + "
"; } - text += "

"; + text += "
"; ui->packDescription->setHtml(text + current.description); } From c5eb6fe6fb733c62c071473a8d1102c44e133c17 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 24 May 2022 12:14:08 -0300 Subject: [PATCH 506/605] feat: add links for curseforge mods NOT DOWNLOAD LINKS! (someone would ask it i'm sure :p) --- launcher/modplatform/flame/FlameAPI.h | 5 ++++- launcher/modplatform/flame/FlameModIndex.cpp | 21 ++++++++++++++++++++ launcher/modplatform/flame/FlameModIndex.h | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 6ce474c8..3b5c5782 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -41,7 +41,10 @@ class FlameAPI : public NetworkModAPI { .arg(gameVersionStr); }; - inline auto getModInfoURL(QString& id) const -> QString override { return {}; }; + inline auto getModInfoURL(QString& id) const -> QString override + { + return QString("https://api.curseforge.com/v1/mods/%1").arg(id); + }; inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index ba0824cf..1a2f2bd4 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -25,6 +25,27 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) packAuthor.url = Json::requireString(author, "url"); pack.authors.append(packAuthor); } + + loadExtraPackData(pack, obj); +} + +void FlameMod::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj) +{ + auto links_obj = Json::ensureObject(obj, "links"); + + pack.extraData.issuesUrl = Json::ensureString(links_obj, "issuesUrl"); + if(pack.extraData.issuesUrl.endsWith('/')) + pack.extraData.issuesUrl.chop(1); + + pack.extraData.sourceUrl = Json::ensureString(links_obj, "sourceUrl"); + if(pack.extraData.sourceUrl.endsWith('/')) + pack.extraData.sourceUrl.chop(1); + + pack.extraData.wikiUrl = Json::ensureString(links_obj, "wikiUrl"); + if(pack.extraData.wikiUrl.endsWith('/')) + pack.extraData.wikiUrl.chop(1); + + pack.extraDataLoaded = true; } void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index d3171d94..c631a6f3 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -12,6 +12,7 @@ namespace FlameMod { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr& network, From e64438016040c1a7ad1834a5735d34d6d1ea0cdb Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 24 May 2022 12:27:32 -0300 Subject: [PATCH 507/605] feat: add links to curseforge modpacks --- launcher/modplatform/flame/FlamePackIndex.cpp | 27 +++++++- launcher/modplatform/flame/FlamePackIndex.h | 12 +++- .../ui/pages/modplatform/flame/FlamePage.cpp | 68 ++++++++++++------- .../ui/pages/modplatform/flame/FlamePage.h | 2 + 4 files changed, 84 insertions(+), 25 deletions(-) diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index 6d48a3bf..43aae02e 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -6,7 +6,6 @@ void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); pack.name = Json::requireString(obj, "name"); - pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.description = Json::ensureString(obj, "summary", ""); auto logo = Json::requireObject(obj, "logo"); @@ -46,6 +45,32 @@ void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) 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, "issuesUrl"); + 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) diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index a8bb15be..c0781d62 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -21,6 +21,13 @@ struct IndexedVersion { QString fileName; }; +struct ModpackExtra { + QString websiteUrl; + QString wikiUrl; + QString issuesUrl; + QString sourceUrl; +}; + struct IndexedPack { int addonId; @@ -29,13 +36,16 @@ struct IndexedPack QList authors; QString logoName; QString logoUrl; - QString websiteUrl; 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); } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index ec774621..a238343b 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -119,29 +119,6 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) } current = listModel->data(first, Qt::UserRole).value(); - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "" + name + ""; - if (!current.authors.empty()) { - auto authorToStr = [](Flame::ModpackAuthor& author) { - if (author.url.isEmpty()) { - return author.name; - } - return QString("%2").arg(author.url, author.name); - }; - QStringList authorStrs; - for (auto& author : current.authors) { - authorStrs.push_back(authorToStr(author)); - } - text += "
" + tr(" by ") + authorStrs.join(", "); - } - text += "

"; - - ui->packDescription->setHtml(text + current.description); if (current.versionsLoaded == false) { qDebug() << "Loading flame modpack versions"; @@ -188,6 +165,8 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) suggestCurrent(); } + + updateUi(); } void FlamePage::suggestCurrent() @@ -217,3 +196,46 @@ void FlamePage::onVersionSelectionChanged(QString data) selectedVersion = ui->versionSelectionBox->currentData().toString(); suggestCurrent(); } + +void FlamePage::updateUi() +{ + QString text = ""; + QString name = current.name; + + if (current.extra.websiteUrl.isEmpty()) + text = name; + else + text = "" + name + ""; + if (!current.authors.empty()) { + auto authorToStr = [](Flame::ModpackAuthor& author) { + if (author.url.isEmpty()) { + return author.name; + } + return QString("%2").arg(author.url, author.name); + }; + QStringList authorStrs; + for (auto& author : 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()) { + 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) + "
"; + } + + text += "
"; + + ui->packDescription->setHtml(text + current.description); +} diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index baac57c9..8130e416 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -79,6 +79,8 @@ public: virtual bool shouldDisplay() const override; void retranslate() override; + void updateUi(); + void openedImpl() override; bool eventFilter(QObject * watched, QEvent * event) override; From 67c5aa0be9c20e67bb96e917cb881a9d953b9ce4 Mon Sep 17 00:00:00 2001 From: byquanton <32410361+byquanton@users.noreply.github.com> Date: Tue, 24 May 2022 17:44:23 +0200 Subject: [PATCH 508/605] Update org.polymc.PolyMC.metainfo.xml.in Should fix Flatpak/Flathub build --- program_info/org.polymc.PolyMC.metainfo.xml.in | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/program_info/org.polymc.PolyMC.metainfo.xml.in b/program_info/org.polymc.PolyMC.metainfo.xml.in index ff4af1c3..ea665655 100644 --- a/program_info/org.polymc.PolyMC.metainfo.xml.in +++ b/program_info/org.polymc.PolyMC.metainfo.xml.in @@ -28,23 +28,23 @@ The main PolyMC window - https://polymc.org/img/screenshots/LauncherDark.png + https://polymc.org/img/screenshots/LauncherDark.png Modpack installation - https://polymc.org/img/screenshots/ModpackInstallDark.png + https://polymc.org/img/screenshots/ModpackInstallDark.png Mod installation - https://polymc.org/img/screenshots/ModInstallDark.png + https://polymc.org/img/screenshots/ModInstallDark.png Instance management - https://polymc.org/img/screenshots/PropertiesDark.png + https://polymc.org/img/screenshots/PropertiesDark.png Cat :) - https://polymc.org/img/screenshots/LauncherCatDark.png + https://polymc.org/img/screenshots/LauncherCatDark.png From f8e7fb3d481d41473a6d7102d5c218e4a18bba3d Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 24 May 2022 20:19:31 -0300 Subject: [PATCH 509/605] fix: better handle corner case --- launcher/tasks/SequentialTask.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp index e7d58524..ee57cac1 100644 --- a/launcher/tasks/SequentialTask.cpp +++ b/launcher/tasks/SequentialTask.cpp @@ -34,6 +34,11 @@ void SequentialTask::executeTask() bool SequentialTask::abort() { if(m_currentIndex == -1 || m_currentIndex >= m_queue.size()) { + if(m_currentIndex == -1) { + // Don't call emitAborted() here, we want to bypass the 'is the task running' check + emit aborted(); + emit finished(); + } m_queue.clear(); return true; } From 8a1ba03bcb6d43f9aae0555125447b4d5cd2dc1a Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Wed, 25 May 2022 11:46:15 +0800 Subject: [PATCH 510/605] show default metaserver --- launcher/ui/pages/global/APIPage.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 6ad243dd..5d812d07 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -48,6 +48,7 @@ #include "tools/BaseProfiler.h" #include "Application.h" #include "net/PasteUpload.h" +#include "BuildConfig.h" APIPage::APIPage(QWidget *parent) : QWidget(parent), @@ -76,6 +77,8 @@ APIPage::APIPage(QWidget *parent) : ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry)); ui->tabWidget->tabBar()->hide(); + ui->metaURL->setPlaceholderText(BuildConfig.META_URL); + loadSettings(); resetBaseURLNote(); From a28fa219d7693bed9e506346aef0fc585dad3af0 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Wed, 25 May 2022 14:21:09 +0800 Subject: [PATCH 511/605] fix indent width --- .clang-format | 1 + 1 file changed, 1 insertion(+) diff --git a/.clang-format b/.clang-format index f90a4060..51ca0e1c 100644 --- a/.clang-format +++ b/.clang-format @@ -1,6 +1,7 @@ --- Language: Cpp BasedOnStyle: Chromium +IndentWidth: 4 AlignConsecutiveMacros: false AlignConsecutiveAssignments: false AllowShortIfStatementsOnASingleLine: false From e50ec31351fb226028fef5d746ecc59c9d51fecb Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Wed, 25 May 2022 14:44:47 +0800 Subject: [PATCH 512/605] fix --- launcher/ui/pages/global/APIPage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 24189c5c..cf15065b 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -146,7 +146,7 @@ - (Default) + From 938cae1130464fcedaa776d9f54898dece14f9a7 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 25 May 2022 23:04:49 +0200 Subject: [PATCH 513/605] revert: remove CurseForge workaround for packs too Partial revert. Handles missing download URLs. --- launcher/modplatform/flame/FlamePackIndex.cpp | 12 ++++-------- launcher/modplatform/flame/FlamePackIndex.h | 1 - 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index 6d48a3bf..bece7843 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -65,16 +65,12 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) // pick the latest version supported file.mcVersion = versionArray[0].toString(); file.version = Json::requireString(version, "displayName"); - file.fileName = Json::requireString(version, "fileName"); file.downloadUrl = Json::ensureString(version, "downloadUrl"); - if(file.downloadUrl.isEmpty()){ - //FIXME : HACK, MAY NOT WORK FOR LONG - file.downloadUrl = QString("https://media.forgecdn.net/files/%1/%2/%3") - .arg(QString::number(QString::number(file.fileId).leftRef(4).toInt()) - ,QString::number(QString::number(file.fileId).rightRef(3).toInt()) - ,QUrl::toPercentEncoding(file.fileName)); + + // only add if we have a download URL (third party distribution is enabled) + if (!file.downloadUrl.isEmpty()) { + unsortedVersions.append(file); } - unsortedVersions.append(file); } auto orderSortPredicate = [](const IndexedVersion& a, const IndexedVersion& b) -> bool { return a.fileId > b.fileId; }; diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index a8bb15be..7ffa29c3 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -18,7 +18,6 @@ struct IndexedVersion { QString version; QString mcVersion; QString downloadUrl; - QString fileName; }; struct IndexedPack From f541ea659c2050b333fc28db582b3848a0fb8976 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Thu, 26 May 2022 18:16:32 +0800 Subject: [PATCH 514/605] better new icon --- program_info/org.polymc.PolyMC.bigsur.svg | 124 +++++++++++++++++----- program_info/polymc.icns | Bin 331581 -> 518794 bytes 2 files changed, 98 insertions(+), 26 deletions(-) diff --git a/program_info/org.polymc.PolyMC.bigsur.svg b/program_info/org.polymc.PolyMC.bigsur.svg index 8297049b..e9582f5d 100644 --- a/program_info/org.polymc.PolyMC.bigsur.svg +++ b/program_info/org.polymc.PolyMC.bigsur.svg @@ -5,47 +5,83 @@ fill="none" xmlns="http://www.w3.org/2000/svg" > - + + + + + + + + + + + + + + + + + + + + + + diff --git a/program_info/polymc.icns b/program_info/polymc.icns index 7365e919dec4f8241110641dedd7f849a9a8b95a..231fa22abafe2dc8260e8c17f45574816fa88991 100644 GIT binary patch literal 518794 zcmdSAbx<8o6fSsix8QEU-Q9z`1qdEIxN~uLcb8zn3GVLh?i&2!&Ru@5UhUhe-P+ne z-ak82JuTBcJ<~mX&iUp$V`XCJ41hb0v@&612LO;$BUF^6Q4k3b0RRAstjssH&sg-o z01x|lEVpvp`ivl))uhD%)l)<#pP6rF+Op<~iU9i0G&}$z%nAVgUzN{5@EHIAsC)Xu? zCW?@bjc-;L3o)B3*VY&JluagAm!0k277tTLp6%}LHT4Lwpuf_$Ega5M&r^IH&d1EN z1Xbj%6t(4KV@WDd+~ zmc5vU8TaM`0w{@4MW7C$XlZ1Y?^nR=RM27&bui@PXiB+)qgxr+W`__G5)we)ZZK8L z#xn3b>)5XwuT6w4T5ZT+)R7lOj}vif{5{IKJILDD^73+A9iV8(uM`XH71nIBGc7>h zu8kmhAsizUlh6A8bq8%FNmPPf%zr&YIr9(7NSrP z*DlY=Q0XC;u&{xqfx#Baw|7y_O~H!G3`D@4rHhk|O=V+K(?e*Azm)`;hzPjivGAsn zG5J{K4=oEF##yQ@U~PS!SgF)GZ?WgKc(b6~@ciFDm8yfr4d#c-P2EG4WOMP2C{wO} zdb?prrO0wSD?2*_G&VT(h+p`SW@cuDweiW7OH3>&?4!1f0xq;;>|&Kz zi$f4hyyqgkUS$QRqf%ioxq%ghAaR(nO zJI8srD+l9C@6E6GQ5SdhZ{pTv zBIZV|Yt+FD?RN{YP#RiIAuhoK7f>xsM>?)e4=D2v1{&woVZRfh?EH*DyzeJ()BlDh zG>xskOKFpg(KvZPr{SZgofbfN^cGZK&335sH6^(@7>aZ?n29c`VE9y7oWnqc8__M+7rqCoUs19$8VIBX9{+pT{k#C(Qn+<-; zaU+Hrc}e;dUTFT1<0F5ZZEhhShmkHE-tGQ|JBk#bztkRLrgEoA%7E%y#*vC!yXg=d zkXh4nbnoW`B=qm*yDv&B_BTPbUtb+*vXVI^B4^V%*=*UNsu^TPa4DVljQTm?dna*@ z*r)p=$T9fR_H9opSs-@V6zodFj>I_vX_n0d(qQo4Te2UTXtuI=wimwE2w{$uUQ#RB zegrBm$e;P zMLlOQ>OztUn2zZfhYiaMY%pnHK{IR&8&6AAe7+%pU{mICJ?TW8BW+w!yw(krr&h95 ztYw0id-z5V67sm|SJg9%FWiH-0XN zsYE8EiC3vf;v^~Na@di3px@+!LOloA=4XYBRe807UXZav2e4V>8|T?ki8zre??+V) zf2}x<2D~7LHt7xrptR8!bEFSr(Do#yanJUKAVS_KQGYb=3`U`BU?(Cn5VF}eb%i@a zp+jH^2KR>!B~*((g8}ewXW?KtQs3YXVMC>43lKYGPoPB#IxKb!QxX4WEfF#9*?hPy%L5K9R zgAYdVqK+d@KoFMmNv0GRA>p5DD~65jqheVCea2;fvr~y4g#qTXyQe>u#&DYh{5@%; z3dqo267yb&R?I1(XGm6GNoj^#tq}XIJ$}IkG&lc437!Tz7z~Im&6<+LJ`60X#g=DseNOZb9B&3`$w*zmwz3mxM6f_%3ro?q! z3VA>eU;Q=r&Df}?!N@-C)LSAnlER{({x9OFSA+FiVzSd=;T(F@+fvLwB z%6<=KIAXv)4M<^RZwTY%7VXNSI#nV7B)-(|2p>L^-0%`}wuf^;h_YM0X||P{L)os? z30Z`Zj;1-wqxma5;6f;jAQh}@$rQV;143h2RV>|%BbYyB_RIida$hHgb!y;B8;k+2 zCXOczIx=!QxG7ax&DgPwm#GBLOhl4`X0j7ET37u;iSGtPJ+xp)~-r(Qke5PQnG= zf!w;76m0MBZubWfwvjWNE`n)R+R+s!kch<-;FT!KM!x`TtxnAM2F!5b@3b}zUhdLz z4RFrNBu6&NL;Ck>O$o(4h-%26$|e*Um{S4JB_-6{A^rSa)#W;^w(M~J`8rT}8v+$= z`hKQ8`QA2eZrnzLJx&AWNC!XltwY3E8r|ZvIWE~zaIUE+DmBEU~nx`WmkfWdL z{eBd0?M7U%Vfd^E+00>0*)!vTGBCK|4uCuZS7Qhsm&5Ig_NS_0Xswn2Y}tju!o~fd zn`SS8^7no1L$ZgZrmhBVw4@qCIA^4{1G|WpH{*d$KYxNi36wRr3*VEudsqy+1iLG$ zY^(p4ogn0wZE)GwCfbEu5WO;}RzP+aQ>(%|ZicFoHczvmr|^5yy?gJ5j8Ye9VM7pKxQQ_rKGD?7qGzspvdAG*3C zTB@plv5`z+f4xVktH^uT`@21Z8nyVl(i6sl3c7V*g z_I%>!O{0txQ;X002_6{|8j! z|5?rdf1r|nVdL@7%>Nct;{Sg^CI9sRkcowM2LMQh|0gQZM~K&#cJ1`b$`ED;ZDgBpbeDt^8Dlszeey))r%oO(^KyB>;ODE!JjtoEvX9P<^=A8RFE5|>t?*#x zWKr4x703c;afIM6@Koq>WdE-P9vt#y?3*!!BqU0KVZWXTo)DkLdV71nG2$>tYDc>u z8$3r-R4d(5^l$-tl#l7u0{V@M!}!BgNs}^5$!|lSpoRr0?)%*ftI9kCw*ou_ycxXZ zzkWH9;!P#l6lF zwvfQ$^eLX?844s>a$P0g)+~vR(KzYC*{m6(2exe~>%^VtMY{4=NuD29CtplA`qXJh3^2+?Pr zPbCh2s1+JHt&Fi+YYC5s;%N-eL-QrV4>q@RW#T*I)ms7^%@xHehTlVd-eCTlec-Gg zQc9k_k}ui0XuKJM0G8>i4<3}e6pS+r2Y?>jc2@n0n(T*}jvV$DST>R&Os78bpv+4A zwlh;80mBg(s0iWX+nc_7c-Z>0iitoY z2+7IKUHzx53`jy_(kL-Cn!X!WQZ{hW!k$H^Y>Z5UsP!WxKM4(Gsq|v8jms3+Cf-Xq z)mK~J+3AC1+Pz*ta_%lp*)z$rO;q*g&l3%O-tcdmKc00-bh!+l%}>r7c!Wvxu7~>x z(Q;W>TK}O-l?nnS;4($B3jrm8PBtjrz~dZu>IUz4)-2T{$%?YFleN>+Q)z!9gyZ*) zZ{JzJo+DdA2V(Y%1fzaX((d+9_DBogBxqY%ouu`A`}3ARmnX`1;ZQt!fP2}pAKTL& z_*vt6f0$=K{06^)ym~?*U&weC@MVV`{V#^9>@Cbn_Nc-*20a@?Ea5kkDY0| zmQFyAsl2#w8r(45D-Xt|f2S2SN+P!R>Lwd#b$dUrT1j-fp__e8Pz^l_`h`bo=cDHj z1J_yiJjY){l0x~jQje_CBX~mCCN#fa-}H{ArOqv|BH7x}nOu873k1Z33s?Pn4h=vC zyiRMDK-RFQpG?-zyXo;Y+@5yZ2@RPiS4 zIhxE$TKTxfvHxB=TxtM4xy(wqkwscUjG$t-Y6_>axKyP({ysML82{ICe@IQ^CnFOa zhYtQCJAo(uRO;1WmL>m}2sx}=I}Z<;&>a@6i2mw<$R%VpgX6n`K!>n1isZ0);;t|GOB3lj0 zzV~a7hYGK3cu*Hs8ANDY&2F8Q5jZq(_WvHP z19+z$TCyWzy!Po5M)|CX57yo^v6+btgQCu;8acHhPRmvA|G3P>Q{r?iJ9YtCT&b%N~;2 z^`HBVe(fH*5b^Lh-U?mA1Lqi;L7OJY#%s--$O?nw!o9iU$34DLZib$*qdh5)*Q+#} zX4TlNRv z;{Z^Faq=nnD4p29xbdmGO7d0|gZu98WwIuud4x+uDvkEF`vmgEqlr1}fjMFpyD zr;q9S79E?Z2Vla>E}vOq*$JoRX?!>m?F;+vkl0+EPR1+q zf}=Sg{B{(Dor$RkT4dWCg7y#i4;77h`Xj7kOR@*e^>RfXSf}t%N2GZ+zNSrg&M6?^ z5)cJJ5L&$5?HDGaoWApd{5d22mug7*oY3KNqgwdKBYC*B`lmj2R>KKI>{!ec zc8Q$JcsdDcdJtdYPsb#ulnXo*B#flmja&~w4$G5Y$yYY7ksZEn&2N`&({ehhFgW4r zf&_T6x33umustLx@$0CXV)!54fIvCnusapA4EP@k!{Xc$45o;1L}+w z$hUPKCh$5&s7y;-HnG{yT?74Iezb7YmkG*t9}GI&$B6iNJInza6O6c)7`z#ZJ6)Mx zuQ?v{kMsJ?h%DA12$V1#{=%jP3jL4_Z``BE)$bj;Gj!vIh2DGply-7QYSl365=QBZ z+M<7H4JhFLQa`QLd}xqtgVk@TSx8kTIe7}D0H{|vK9eowuHEM%x5&uk+~%5!uC3+u zrYE!s1d9Fog9JA8>j-V|EW%A^R{bm6glYFFh}>4gn>>XFNV) zzvPZ5{?PR{LqrS1*153u(t17pEzz2g$R1J>ZXPw)dPz&qW;|Zk1p;UT%CQzpQ8C(!XG}anoFN(W0_69%N)yaQFZ=a%2X| zSmQ>Fb6vUH@We268BG7OWhq1f{+Q5W8*AiKCm6FRA3jKkB;}n6a+%tJ75x|~IwTT{ zo776SaX@6((V%l?NQGRKUmSHDBS_(kbY_!w(i1GiI-;j*{u*LchhDOeW1C7iTp>Qx zPbrvWa!J*gCT}JiQ#KyOQLX_?5L~g$?P2w#2*&|H9IKMWpKD+@I=24QPh8Uoe?5o1 z!KwUR+v%eaN4%7~ECHTezIGMY%TyZXJr3x=;PpA5Is1s!r#57FfrQ_Pb!ky4j z1(E(bwi~kT^O$4-wO8HjnzY z&mn)n5t&6UI8|xj*}8!Gee?}8_A79nSW6`*2n!D~r3TR*x%U9?o^TKknI}$eQ3Pi{ zuflR}rbV4ONYx*PFwAUW{W&_$DVd&UMes2I0{Mz4UQWR_&Atajx|480y9T55Bn(IE z@Lo8QVU_P0tHJn#x*qi+b?!3wE(KX$Bo>;#JhZr;oj#&BxCk^H#)#FvKmUtlv@mco zuYzMpNKVV_*T0Y~mp0U9W{I?nShi?Y;S^t-D|}9}b7<5LiJ6`gU!AUJ`V0>H0_03< zQ~ybHo9#{AE1MLG((H6O)jAXRiCOyE?JB%)tC(lOTNN;G%Gpmu$W|Ty=$((mWwqvk8cJ+ z$krb@T0bYHTHR!G$Va6;R$y%Oy&YMfr;62nq%C@1ZyRpO0KgS2zSQ%`TG{-&{Kxld z_b|UGU-Cn94|ovf1^`kcRT)DGs+h~U*lCN3-4rE_6FL?i>5R^5n94!o4|229(;N*A zv~+=;y#)b2bx1h<6obp8``C|AfBHFAd8@*kWQ;m!($$DuKaynLFD-UtDT(sTx^%cq zEqI-IU@<%MRwG!^0EJYj3;1f_GLs<^OmsF}EkA*nt;x3|BmR62UCDEzu`jZ8y+OEz zL|nnhW6de0js(F&IX9dHTF=NrB(g#Fq3%lg)zFl1{y@Afjxy9;$;8I2CKweWf*Vzo|I9 zE_Ot+l(O!L)c`dh2lPc&vqK@0l`H-1XSG_)&&^1VYx5gDs`a5AuJDE%uYxxVW7Aia z8)$D1!HDRUva*;o(bUQoy(6X{zmWZDU64B+72!ha_0T_MiH&)q+T{Evz+LbTCD6i? zm{j-RH2B(>4eQWOowN_CLHb6}|9d}KIFIgIF}*4?_R|3pwq;XPHF>RcdEoG!LmFI_ zp@amKAl{*Xrp$pPC>TT3Lm=kX2Z!#4A=jEmpEBa7g0g7g{0!mp-M5{h-DNZ5O_q$l zeS!HN`UqTs$O%m-l!6ijYp7`0==qevB=(Ayv5hrN%G#9bPF)GY5=VL;^jxd?_Of#= z3=ve3se@7W;zR7ZU;COIZcnAzZ-fBlI@)+yH$B0WMW*=3D4fE_m~KyQ>mYRu6L{}b z&fp6zirn}%(?&MfoG%)!Qe~E5p#~C0S6udxxAO4k&!z<=NDD$_xnT<)d(9rJW_3o_ zb#!e0`2_gGvBSTBcwjCso6CcPfiIG3%{DO&*&Zko7f+QpJ(kxA;YA?qLXFwlyIttN z#^liyI#iNx=Az13C$k5sF?fgig(YGR?V)0%;48wm?WlP0RXf~@-Z}Y6&RGJ-(Di`* zC$J^928A%nz6k~d6&5no5)8vNSVAAS|0rtTRt*;-s8u|_f0IjJ6(OUngIbfS zQJ%i76y`w+<(pGXO&vXDsBu&JK%^@bpMz`$BAb^eQi{B=7WPC(Yp{OdnD03cQ4_XE zP@RJR>K+~111BRWR{1MpWP^vI#e_9_Qw*{>_OysULR|_p4O2n@;pKy9Ym$k`BLx1s z*ETaQaXp3|29}iH{`sch^;9VM>)$6_fl=v_Gg6bFrOc&eE9`L~IrCTOaIkg2K+Z48 zfDPaylCad*iJ7R#j&Vn+)epwl>2}=P5?7@|IOmR59%i9_miz#VEY5$ z^u|S4A^(JYMO>0=?%zZQoe9FJJmh)PH&ttp;-rgzWAh)@L&I0pHRv5_Ebs2BW1R^o zuza25c$;VaVt1SVFnbnYOC%k?msnKUdPB@{*&BCb8P>H7I2s2+R83a%Zu1@=M)Kjx z12@G_c)d1=FpqV=U-Oz$IjA(e0dnz;0#>5Vbl?QCimadW7rxOKc`WBQJL;vqtjNbTB}&xVQ}NXsjteza$b_wF1|&sBk~3ULk%(>TAJ2PK$pr-&v80RQ~ zH4lBw9hz|=S{3z~iCCy)xjSG>xj6PQMq)|lJ5kyyHkDd*&E5+er0|od*M!oF-`R`$?^$cR@OY$_XlXbuZh03TZf6~Qfn&dC6-x`pek4@QmZX{X!i%oj@IaYYX^8tF0*3Qa@Jv!vKBPr&nPfpgGa{ZX=)8 z;R0&Nw!UDu0+%<_OD0z-;ur^n`wb;9?n#$R^>cB>6T~wtb?ehRKzv=Nv>D8iE%=X^JM~Lmlm$lukPXagKU_gZQ0dfot_jLq7=}NkSOHOrl4s-LU>`<6a;F=*@_-p} zwQme#0+MDs8o%J=-HA7$sJlDQBhUO#!1;5^ZK~{-E7X6SqJu6*vD-dR4Mg^qTw}9b z{>nNBAyZsp#8N>Nl^8CZJI=v;w>TN&d3x|lVTEF_K((`VfTx!i9P5B$6okrN2eU!_ zlatBkB})l%I(@9A?Zv!&xT(}m26-iRAXTT4VIBNkp4XaX&F(8t2tU}Q`_O&P1|Rho>Q_5nVg zG@;igScpL{_SV+7Opx2KGxPKPYAyZ+9T#&MPCddqJb`ve z^s;{K&B&8`<+$P5ca_*ac^uLZRtv5cOd6KF**Fa5z-ODCus3EsukPp}?_)PC4U_s2 zHRHYWZNma?d^$HbOW9ydE9nStBfSgyQtVM0g|-|hK>23RQm%%6V0=c%XQeupG@Ync zS>%SsX42dliQA1ki9d!2`5bO8E<|E%+Z8ipu&myD3uQh~K0YVpqb%LzzQ$BN?Qo^a zd0TMhUDFVOc>aP9D<-e)ZSCu=sPLP2a{m1PE>BN=K0nJZ+8kL;*^gvUTK;8 zzkNq=fbeMO`jDl~tEqTV6OMpo1Edmmyl#1s4)#hoBO^yUZcMLjMH%(iUHCZf(nh;n zv@Cs&={1={mBje3Oqaz~#}I~ztf_%GT&)wUbeO5#?!wN_+WwJ0&Zvb*;e~i7op9W+i>3YG3N#$4I@#_Sp(`Q5Wo(h>Vnp}@P4M75-4)e)OS4}Q1%{Mvof7HwYYLzu> z>uXa!k6zoa?IPf}V^ZkBTToZto7rBG3aseb5PUB1VKvm}bN%Rb`@X;N)myMc?AHEV zNRGdtK)3~hRN-6Z{-UXUj^GYGF-z_dhsIW23d>ibMaL0~X%V-(f3YmS&*Of#RrWq- z%OfQ>G@Mt3-fNCA>XurNf%QAb;D24O!rm8+%O9%xwin`dm~6p(a?CC<_UxV-f`%N{ zc6P$ew(Cu#es3>lezEQ8G_wk*uij66_ZDIsrg#ASeaR&Hr_B2bEtc-XC0!9v6GM%+ zpR4P=TJ_KSB*qf~7shMw%5?@J+WZ6s8gX%B)kTb!0q}6P#_z%!O{md!{R|yN`MG6> z;VGM62UgTI(d|oas7k9*Z(xXF+et}`Zj()`=hoYE#b1-`Fk%VS=+baJH7n$!LPb%M z+ck)=4W!wVjnuA;(Ml8B9E^XcyB6hz2a6yad|4ymT|OGN+c7m-9TW zmaAW+Nr2E60yKoG!G=@^MD|@u;SOq#3+i;bx%^JQ^4T0rceL`JM$x+bs>t^pqfsO3 z>evNOq7|w9{&+eOEsK0If)XwD!{mZV$C}J`CoA)O!J`E;6%w6!FknjL2=RGt-~UW>FMvhB zHRbSRXObOE1|8A3`1tr4+v)LvaR2|^PY(l9J?U~0JZY$@`HP5oe;oSPv~5D@y^f5? zcv)D`fT|MXeCT+^bntruT|VBPmts-FrDezBqs)lgByR!7PeeM=YW(4M!0;Nb*N!GzWIn*v0v+Mf~ z%p6}RG>@4`k;^Im#jv?ecJ?@3#^KZ^Tfpm~hiYu;x8;5IagXxmX7}mjWF?M*GN4^1 zr<&3~`qs8?{Rtl)NJFJdr)`&AHeT^eACjJ)1a6O!iJkDByky^NlM@k6)^|iGREj}wA*&Vc6>oB8I8c4Z0a^J+fyk7H0Wwj%d&>{W!-3L!h+6my5MnpltR(PLZMn*e5cUEFQPa22-rfp!H z)>A)mNH8oX6|}FDxCFBZ+MfWU_PgHh_^8>==S738%*4gmwSN^Asrv11_t^sOm){XS zNXeftp2(lhPH*x%!r@lHMJ&|`lE*cpYdtxy4;S^#1(E}qk({>X0E_9|&TmpH(@}-n zh-jfb_T|2uXE6z4J8t#DwYIiSbiSH_Lb2Pi8_ zN_U!O{zCcBb7Xn^_1Vvm^QpqenPo(4IV})O0oL4s1JWC9^QOEr_%Y$OZ2!50UijW- z1?x{N;KrEC!}}zO&vYCUH|kEjV{~DfQ;HT&2~7e_MTjUR8ZW|3oMO+PN>T#qF9Dg7 zQdy{!^?lzL78Pz9BI29orS>2kM{b?6ICDENsGjBV=WbIiI)mFjD&4*=;T}+%V(8|Y zHa6C1Cf}YYN{?Stkp~3eJy5D=Q(`>Ini!2c`l$6 zm3&DPr#t~%u|UHq7Nl+H)9FN(Vj8sPpWOdmT-O_-r(Ol~|Go>y9R9r78I=&8gpD!T z3adrqC83q?3#n?XvfT|VC|D=)2K9cxudj3~{)HvG0)anvc6MyJp9Zb=+UC%d=lT6_ z&8TjyI;>h`OGg*;hO$B{(%!yy2L37f&^x^6n+HyZE4)UHSYoRuEVJ|1+?Sy8M+P_r zM`D>7cYn3PNt~X2Z)9`<1HV~rY==$*nm%o!A6rFj*vz$$a$~p-MS0;+HA=MCGag5y z#1b^q81Z#Q>ZtLYM@*Fkg7=u>-AV*;f{At_C&3ufH(LAFRQ5G?)rhAw3cr+`nhbba zbSwG&k|`C65;wj+2V>>sIUQCpE0YXr0C5}Ie8;RuI@Acwe1K1)sLz&@dFQM3$1zDy z&QhG70Lz3ja1}cRbD}cKprW+OT!H@!0Mlxsa6%L|_D09a>L%Xg=J0cJ$w8Gtg8nIE z)~n_TVZ^@cD|Rt7O3eYsf!+tRD#wkCH}|R1#AeGiuuNl5Hw{|*8Oyj_V9g^6>woY?qZr>@<@-gLzQS6pvH)WD=#x!Q*>_QCE%XUtRBO7Sex(O_CYvPiIYjn1B?xoXA;} zVOWu#I9l<1)gl+$>gPbR;QP$TMh)tjn~%rtZm-LYRxr*yS!{4hnM_ORsrN7Vo4bL9 z9h(QzH3P+0I)-|Gv`yD#{dAavT;A}*AJ-2T>wd0_<(k`RJN8v>fRROziVjFLMyN-p zr{Mh+Y_yw5)g1MrX8{4Mfyj4cl22oP=W)76>owe;!dg%iK^v&|@9jm~@V=dVx){|{ z--7BH@Oj*HkLWmht#&w^z84G|%Upq4p=MfIHN~`mnP`|{JP@XD~Wq8NlwTB)Xq2qI1FNIxs2^?d)-)S|Dp6S zY6FUH1-~z%`91aD-ZThILm0hb<~E3Ka0-rQ6HhOzXzWOpQwTAAP=o#tz-*{9s#um2~Ud-!Ag z)lEg@?3Vl_ner>?3FB+L%h+E9;cf*%@{`8IQUjQ4DH~m{*S+S>3tMaGm?XVN9Z?b} zK&Iuv{D{ZLw#60rL;?M+!*EHk2;&#Qtq$i*24`Gu2}F$*zp@T#>Zd#^{PL{UCi)0; z(K&v1%ipjBuI1a#bDwn_%>whJ(_9T+J27tBlv2{B!5Q1Qcl*0uuo zvNOQLU^PR72pwzkyb`FKpr^`MamtZl_8pg`$MruK%NKIvX}|n3qlc`3M?PMh-Fc9N%J{lJ9-z$>!T~slQvUGuJA>ihUNJI{d{49yUw@?SkB8=I48`!AwkgQdPFSOu z?vs+NqY+4>LnzVvE@XmF-IxOhvU-cr(&vf~+r1|N*bs8ttjsV__8O6(Ai8ocL20*^ zQ)@C9DsE1aMM>T?EiLuN*n7_Z{9Af?IbsG_X>r_#e7=!g6#&RLdN$2x6%?B11UVg7 zEROL6p`98-ktB^L`71rkz}rE@kGWb6= zY9uvl=I*PX2j@NyZ-wicaq_GtdfMkIcz$QU_Wx8^FQ-&!ot0tnZf8#4->q(vDNWCk z>c1=kJlTM@VMpVA^}>COT~43_h`Wmg&N;5?jTgG4A!UO5(#AZ&;)CAfarKg_!ooPu zQ-m`iDACvGx0~s=N~!!_-X6;gY|3jyE;?NW-!IOHzjY3cwfN}9Kh$ojm~8hR|B>Ew zf#pZ%xo^0FI+`NfFw$T~OJ7MGb=V1%w|9u72I#TC^a9l4!s1V!>MKtYu_s~ga#t!=KdhtLv5RszWu*6X9mNwILJ0my zWb;s{2BF!sk8jkU)J*Pj)MUgqG0)Ggnceohn~tYVuB?O$hN14e7Aj$%rARtgLQF*K z0#;$tUyox?MgWJB(M_n%patgEQ*h9#uVs>;#bq*C8W(`aesG7C8BDU_8wb2!i8@+d zelO4wJpO!0?t9(9oySwbnYFpVxWI*XNc~5|A`GTaW64rsAJgkigDK2|K5bb>hUqjk06%y*s{3d#k`urs5a zBTKt__L%R+gL%HzQ!9`QfIsmgktb_M?@?w(yqh#0wPPwNj#qqd6fON8$C!Y`leX{{ zgZpK?ZSKHF`1IyFJ}0uHsZ0-u;HY;WHl4?gX7e5i{J9)An2CUo>_G7KKtCyIC+siA z7r|2L*erXS58d0bS=@H8r7e5LFPr_Es~dRsR0(BF zoW4qbSd;d;6wkGj$rjY zYeI--g2mvOrsCMIZLi<_@Xgf?g*)~dozi^GVGVor)5sYHkUavbh;1zB6$o*Ln^IKg7l%QV2j z;EFZFiBs4F^LYnPLQno?=P*pX5y#f=h-Otbcs^;b}yfk`O7X6zAU_xzw8vyLyQA_ zK)SuA-<`$%Hoh0+^^XIn0`Pi&52RYD@RC|GjJ;qxM5FanQf1HD7spB$)ur{Akx9OV49lkbi9gKIIW|uyvm`CA)>v1?Qi8~g&^OHnaUuc08fxG!z!i&G?h#g%;QNat{8E z`RyE3I4yz$R~`RZdD+Jby~{Yxw5zGbI?3|wnXlHV8^j+_69+-U<~X*VZqpD(8l;v& zgFW9#Gm4#=bG_w7%^P^S+a?Yn`~%9C2TPxAHqp!r#G(vreN zYB<~A1ub`XA6znRDR)K$UkZdA=t!rAmEmZ8^YsFn$!+l7cE+&byJIils~))`Cfb3M zS*j{^UVwQfuTQ!8&o-Zu_5{aypGzp5~|jhq$>aF z7AE}=a~EE^hF7+F#(uDHouMNc)D$TG=8v{MCw`iIGl z4Y;eyke%h+Yrw$v*@jeVg*BQYk;+;(FfIZ?zi@*Uqd-O`PyoR%oW?~rqJoaZstDO@ zIxdG}+xNlQ{CM}+Gyv3Z{>R=*n@oXd$)!~f*1yEsHHi?bvw7RDiNw3_?O;&g&~>vj ziR9{p8Y{FCx;$#}t7%3MRx1J}ktwIvMA&LzMRe;QMk?v>>%g8aXu(Ktr>p4+=2p$; zNd|j1EheA+i4BgOw?vqoT)C32lpBjhT?FAgU4J>o%)<_kiBNqvG`rXG=Mu(hV&YHF z^YdGMdR)gn%62@OLEfjzZLG}@TZm4wbV_O6w#6W)K1Ich8H}F@JOmC2Grkr(cfz+J z(qNX`3yY7fDP;5Y+yJ>i(yf|gQuf7?&`H3IU%3@XA^GzM4c`0m=W8Uxc-!Wz!`aM^ zW_#S#-}6LpltGg@@9@(q3&xd)bN`Z|4aMDm1=Ptp>j;O@Kt&<@-s}rNX_LL}rq+z8 z2Odv))qI#Z_iik*_edfIFJ^;RhWO2AVt?6Ge(#D^jM6+7hBC5@q7?a*w@bw}sC0XE!^;d|RfbH?(|{0ux81pS?u#$fUuuuakyLcgYIg z)RcKb!PP1p#%m3@b2nX)(oX0 zhum>(q9osb*=$eynyo|2z_2R2eGfFvGA406p7moPZ zo2-ojvWw3%FK*10bn9^aG3#?li0pbfe?1e~1|ME0_}vU+Z3WHUQ*Zs1C*Lp(FMEH) zbr*GFXqWzVwQeU^izXKJhUJ(?TA>NvOr`;LVk><#hu2YY&+x#4u?m;)eF$Z&1?RnZ z*|tI0dy~AP8tosgMu~@a!qew`M{H)xZ?vFpE^+5OkX3;z$(uRrp1?FA++Y=@X9lHA zKT-q;`qK$x&RyLJCoOUm<-aTCcami!5&TI1#y??46q!HX?A&lS?|rsPbz63`j%8Zs zwRvVCSeGY0*O+EyX!nkMKMw8RD!U(Vm^#&l77d6sM^DNxH89TnO`ko?_)ys*sQzXg z5NPvFt+#LC`69vox;fQw(kF3z2riK)cA`$N3)UXk@8#&6ak}_9O}U`HM0kHHV*Uih zV>O*CivuJ0L(goi%^fD2gHKrExAfwV6iXy^4VG+!1+6I^vSvp!pKKve0^_o(I*4GWqNpBY_G?$(NrO|7!) zvVukST$^>@g=C96p-f*UWn3gBUNqzqjQIc*?FeN*QkH}sh$#{TkwGE{ePvWOA^rc2 zpBp<%Is*A&8OU%TKWPo|O@x=d0Op-|wQ=J9f*_29?NU|*z|u~bBGLZK@HCQ2+aBFf;7kNu*_npY{#(8Vt%O;^8_0rY>=-?yM^V5q=HVI z*^lp0T+!EUlH^H1$ec9vpMZPbXbp=<#oy?Y8+58DE3@FLq_Z7tDE4RR7JSLZ_LC^G zqR#tx{0pU#BhsW}?B!Fg5C#WEjqKI-tq`FI3AuV{Vc8>q12hcz0wr5mJh%VG1zTHq zJcE0!h>c0!gfdEIhru}WNKiEk5>;3gy(U@OW3|j*<*`3|EzmC)SFLTKSU+lfjQn!^i6>=T<=pw{=mb1%wb{U`E z=4X4cbBKEzdRfkeTCwmg_a~mjgZt*7<`C(lySmmi())4m{2h{4Kf|9}eIx0_oAYCn zE75Cl#Z)iCvZVOviDisue~5i9{5CDR&XJ;16E*f>Ar@m;1(bG%v(%-637AFe< z-vof(j7ew&&N_;nG~VRxOR-9swD;#yGD&!3r zTyE`89Q`&OvDD3e+<~YPal3qb(?{*&&$WXGZU%kC=-*I6i6HiU#`q=U_(4AFvxdr> zzt^#;(*$XBM!AWHo_m$AUt5{EGp=Q9_Ld~D506GB5bo2i^nH$T8Zh3fJs`dAz(9-z z-u%(X_9(LXCmPFy7h~l|GX@xSPqB{4KRgzzo4U7z$ORGQ*)^;~8^g@}{Kh zJeV$4fDbTIEO_pnSQeM@FxQPyKq^zLUr=M1tGW`h42dVZT8lWNSdE72eBWpgEv}>u zwgFuRxt!&>tDMB$^pZbzwfXiL@QM1HG)>1uN->DzSpyg-ibs%_8h=M))S=_bv3;ic z(&8(4YN~R;tWL9^q<)NtAx4JHXKWD;Pxll@ytKT-e_Mk=J^Y9(DMWYM@yD8_f`VR*ce1Rj zzz?9eB{j8pUIzJUn_UmT1E34t50}OwQn4?+ws&3_Q<%drsy00hzTrnGIBM`O%A&M5 zFTM^m1hC7gZVPZGp93ej=xVDhS%wXarI`dm*LG`}|NJ;vyV`$4ny9YfU^PQxQ~|KM7W7MRKo zdRQhRXlElD9nwD1#C(^-tj#ip zOvc{asVo90qqLY-IaO_iajd2$#)~^hp4>_a?Q^w$m^~n!JSyj>c1DTuW%qBE`atD( zv$RDHUwQ1)?qJ1yRIH1~0evof-k&QWj>|Dj==xfjBCnk}y_cEzy__G$M?#FInK$jY zwxh(dAa8LJMfCKDNdxjM5+&4i_vb{zZw1k1wH+%`3f?9BU^~%^wC6E)SE;?*QQ7Lb zza~1XVsdG5A>5#hNUE1eO7xj0tv|FCeNMGaDbX+?xw7Pa;&!zX%y1MV*Wb*+%FxoRjjm}#Xzh^SjenYt|8dfa4^!k~yLyaMjFonZk`9<}8OpD`&H18YnFy2ouz2|;o z?xv7{?dl1_57#XSjJIiek!NHp`!%(QFtltoB%zZib?Z9f>~K zd+PlH$9Rdk#-Cq;$a7W6dd;0|0y1u3Y~XBTh32f)u2YMxcC1x{9o^E2(fv6{7?+Oq zN96oy@E!viN0sC#6mZsqE*;^g2y{BBs4rO%&rN!Rp{`ZZS!BpKN-*SJTqrG-e|(%; zu=I!w1(s~?(m*{`8jr8vGsrMx4yTfQBU#@t(u(ur7-gmVGo{p+_L*OvcT(LP?WP5< zUuS4rPE)}E+gQR)e?*T%ixa)-In(xjG$dG}+UQxYcKA>D5?VnQLgTk~jbv_T(#Q4} zk=UM=@7B-kjYt0k5}w@hvUjxGzRSkZ$=IYbey`0Ns~g?1 z=nrKYP5-R6X`b1TkJtQ-ODXG+^+&3Eq_X6D_0#Wv!#po+{_ZQ5bw_@uY&xzSyAotr8rslGf0lTr{H66*{08Nrm*l$hPPp6)eq$u^ zKDi|w4x2V)%l`xhIAT&L*%19q-ulV!S5T^_520IKT(J}Mbm2EH{eHIJu+I`5F}k6z zyoi70!89ajz#OuXPz@uc4`*9TgeXalN`FT!jh4sK1Y=KMe8r~34d22`2ElK+ zH|pJ|58tXU?=lp8s*DiUO>lC(jtM`M|Fsfp#Oxv{1oymg-C&9v;$Lh zIJ}ku&h)3c>00lF6%8ILdc0Nwy>XVWiaR0vtxj4w@Q1l^!uXoX$LX*8r|`G>n*Ejb zp8e-t+-B9C=NF)T0PXhx8f?IBa9v(Wz&dY3=eBvnb($Mr{%*Oge<8TcI4FMKlw7Fq39ySp1{DXMU+&yYuRxY~kdSP)@>l^?XJe zOE%>)G7@mlXR_QAZroFzT3K*N?{QQ;w%hH{EY|qae>p`&(8hN(nGOM4p~(dCcO`heWv-zJf#pzbum%8NnGCSo!5Xun-ZStq|5`m;rb5P?sTot z*2-r6k^8&5a&bH%`=ywnTF!at zflQ&i3E7u0nw8c8yja%g+n!%-omkjIX?kd#R?*kjLgD^}H1eD-8XfAJO#q!;p~$BOcj9*zT4J zzntOwnfVj&b|bRc%QFkTqDIi7KE4rA%mXp`orx+eae0U0EADxp7N9kr3V*MyYnC8j$529?242{m z=n20kk2CO=xSIPRRwr{a!1T-NokL4QsALvS?tZ#l@SR0*K!0wAx-jbsMTWZFHs)Vy zH?c|9uCq=@#(wNq%7PRd0F@EFX6E2mE2ttMoq?Ch*K_quyx4(XP@?IGAw1xVir)BX z#;}77?Ta_Vt$uDn)vpX|DjdGx)bX$aOE-Gs9&;&ve$$+_G*EvoP7C{o`Z0k8F)EOe z^OD%q{1GJSo6+wV-)YqTO>%-q8~?helETEDRD@gzK96`wzBdpD`XbWtlQ%l7UUb*n*m zH|&|)z1s-g^=S@`D-$aUB_Ap_0RLRqDKKR>UB7p@ts#M#ruZGW*Xf3d@E zKS_JIs!Vbe)E}jCfum7gEBG0nyRN-u#>IJ|{3x1jt|?l?Zs)cpf}75C^JdY(Dn_~@ zT+%)1cH>qLOndIfn~;j@YPHH%6E4L-2H7cZMfAYih2|vynN5QuH3U|d98jfFX`L+J zUUOtxKp8A_$_lE_7?Mzn-7-5#CYUPoW|(yc8*U{tYFB0zBvM7~$1&vo%rB7K46_BL zUaU4Z(hK!~eCNyV4@4vYp64z$xf%R!;1l_6m^OvXC()CSnXOPX^L7WJh&Wd*3<60e zQBu}beJVInUqqr;T6lG|TNk@V_)eA{G~ivM#ozblf?#q{t13_;;#VxX)P%(1PjNPk zgu`iJXmlwyB-`Z&d{Rl@lZiO*PtpQ*SN zl|0f00?8x_gu%EV#!~(^%KcT+1%WNqa=~W&&y_%ov}19R<>3s1dAU5)H?h(cay#X3 z4=j!AMiPj%$s~RI(c=kU_gF(JhFp6Qx5<+8%$Y&bJNat=)&vXVBJU6vbKNWFIAe9O%~qfJ1+an)UFgn80q3ceD;z(H zf|o(;Nf{A~&@(LH>6EC4-pS`B#0Gr1%EAuG)c^}c4 zvlh>TcjvfisI}dp8TaZ??yuwmRnFoLPMBcd@ z^5*3fit$E5skIvO+~Can`D_h=8B%=%|Na$FC9lL86szH7r$vHc=u3CDNxG{>614L$ zL-YW}i+N#wngZH==QN>tBbqm~zk7+8+mp=WHD@d0|+8|UoTPG{q9fNouxEQ7*J$>xy zAnxt9TMaLmOMgv26&~{r(EXfK!xNKyy_LR+#Jp3sIqR=y{z?Af8Bv; z#k8)d+h~W`fths+O$RJ})=0#GFd-B9&F(GHd#HU!Z2L6n!UUO~KhtmF6g%qIkKiU+ zvnZmNO>5#9YrrL+_f1a>5`_w_bwyxP|AO9$(zp zWuCtec~u%)vgdu{kNrSw|SuLdsH zdB=XUEczd++)C9X z@kWEbF@HvrnKT&3cE9O=t>F93p&Z(f>6tGq{l9gyGAor4xi{1X=@Ch@``b+ik)5WJ zt&UB*Ssk_CmH-Y#?*wV7wmASGz%o^%3XR!UyrtMS_0ZY@A<$Oi)dCYbChnRoIWU$x zzq+G&@%Dh4VbeB?S002>wh`S0pZZ|WEJxL=3@eYQ%@jZ1MS%yopTzG!Z0%5pG4ShG>;Co`KDz)#m`aFYU~ zEmP_8JG0e|7~s>}`$|6@^8L5Sc>%RDp07sr{oo~i?8sHr6uN3cQzFWoa1dAPYiA(Q z^m!JaGx%1$ITK4uQZ&4Fa1q5CHNu@nDxvo*LW^vB>31l7oE38p>Mk9YW+MIG_xx6N zezf2|TyeJbM@4W8*BJ%gvoSRL)RVRv^w^9nm04qssgN#p`~~H^`X6+8Z_iM+HycoA zwj=WS{i;m2r=S8|`8P%&z>?bK@yI9hP$z2ajS*97~POTF0tv}p~_}nm* z1ke}+vi6yf>ihF|bA@I^i&`N~f{x3X0>*UmZ+99DS`E zF)!Qd1)7g7D@3?lXe}_-F}vk#eeh666J&1`U)=xU)SxFCrg8mt(I^spR7^>758;du zZVrz?c?THbz0c9g_nc8UvA9}cS(jpH`k+t%S(@`qT1KX;>HWhtrL+NKk&+=`Wa{zhmhWnHN_e5N-2<`jX zN3E!ez$n$PZs&Qj1M<(V>n@2JJbp)t@CYr+V2&1c*3C0;u~ABTEXT!-J^&ZloEn&V zqwr{O^^IqJM-p$ZZd*5c7+Yw;BM%bY6HL9Vq0zTV=2ZmNv}Z)@{28VXt_C zN*z6Ha~Y61{o1%ushG&CK*e`eOr?vf{TQ;(pwV@p5sIVOh7CY3uz{05Q=s`nt66^E zlQx>NTGOz6D{(`#07=x1++W<}>kJD5{&TO1AC&DHi!mD%-1r!DHWt2egP*vPkMfnT zm%}m#^Sk6)T@#V+V|Zk6jC{~t9q!2X--zezwH6R$ZyH=_g^xi$e@P z3Bzr7ru3@6cUISo`@#BdXZWk~QQz%=jA*xwTW38lag-51#f4WR7kbzJ?2tze??+0C zly*$8=QT`vLWtv;oG9G$(j!7YegkM9{5jj6BAzQwAUc2z`CPj-O^Eb0_xWLsEml;k zlpsWwxKnzCvb?^82c~ z$I`cHd{Lihck1jg_ff82)k$IYqM?C_K)S=k@we^J7F^@C#1VFDdWTW43^sH9nl14I z>%AVvfI}MhQomuyzTh%V3-g7cXH{C)l#GT6P#Rc6dLe~#Ev`4QEN`6<$W)lG0aR{6 zYeAitj5Fo_1u(-v2@#}`Npu>3U}R)|2p?RFZVK?Sg-*CqMKFloG>gMh?8_g>30$|qS4sb*LZdr;+ zZUxh1OaXViHq~t#WLb|LT!?{zq^x=j}plI%{xVEuP{hw}Y)~eE@>1Yn)v_STpl+?!*Y$w1&^JNu>a5xs9Xm$XZaMe8i_t zc6skGLE(Z6m7dUP{RXQ!8ISvOk*pDjZWI-jAOQySy$Lp%1mvPe$G6(yh8C1o;tkDC}t?H0hy?8m|TKS1WUt4mTtl_CH38$vExvY%&b3YIn%cQX3yD3r5Qd06q5 z+Lq}^%6~sRx4YI7MowzsaHK7Trt$8M_~x4^E5+}$?iG-e3QQc#yEGq3TK=-cpWQS4 zTUWLnUNFPEwteZH{q@n=5AycYXJvKFmos?gXA`3W4zL!44$;Fp*r{Y(V=(m%L}PHP zd>DB-*YfN2;N_xaL1gF1L|SaW4NR{G$#E_RV;=>)<>WO(Vn1ANx8qXBcki}lTg;Uy8fW5l3nxrzb2yu z-(t##OEU!p-bMRnW&iaoM`k||T{)_X>E!@zZ2k;mi2B{9Y-Wro==YY7Iv9`LUF_C( z;j~^Ww`Ck9E3sX2V8jZ_@j2kLnaJbHgzFmcjSXP~_`>?~)t|-O+W>b& zCE%8@(YY>Kk6R4{LI1v8zQ)f!CS;XaipI}B1*sMyRmf-!{z6MqBNGCk$@|-!oG@z7 zeR4|sDjQsHU^2B%bA?I+Akr8AY?}GO_nEW$On3a$u zVYBYPnqPs+RAW4nWTw0@0j{%19fj<`ug&w$qc3jZ%q?EtSzmLVfoqILH&+y*c7jg- zEE4i5s`q{z3y5*{%NtX+GVG)~4b;0amrM&qG^HXZEk``PAVV4-A}P?lim*G2QN@3| z^Al;qD0$TKy@-Y#^2%0IY)E(5Z!(8Qh9W{|DRm8yRuVAup9hs+Q7waQrE~3giuReU z=xHbgJAd9qa-x-~3nkQzJQSty-%7);(rke_?2Q#FwmYs)|6Xy#Xz&f{WNX)IjXrw* ztJdcczwad%#OX1tNxC5t$v$Ux=Y`w<6BXj*>^3P|u5ND(9k1!nN79O1C9%;~wf5YF# z?pM%~07Eus`rEGbNu+mwXSEhhH`b4=wh8_Vm#oTlCxw&zUV`qKCN2TUcg&gc52f0I zY7X{N;w5-Vpm}ScN(}ZS^nMh5YUxq&32lDd8ph4N)u|<>(p5XA;_(oza9;p`X!Sn} zK+6jLB7NfuS@3nf`+XFdpx*-y5`}nuLIy5>?Hv9I?H#1=%~%j2?}8c?)+{SOH%y9c z;!^0?bK>$l!3X7)vPj9ez5P0yUUfuA`{6^#me6yunG=e=-}_5i$p0bV^DHxhk6aTS z6pG@9AIgL}(@ag9uqMSkaT&rgae1Y2x)9uT9~wrp!1p!>U3OX2*!2&q$Dch&eJSF? zevC`dJ30AAz{^k|H1c5ZC}3?8d=Mlvut%|vk8BVY^X+kdkusuP#eHRE%I$-=?{AV zV1ric; zae5nq=$E&p7_RQ({gD?pAu-lB>S0_uqfZQ7QNN2^7cmIY$&BUdFuzns&>_~SXdr5? zAJnuYfxYbcC}t~5ECpBv$Qg%ndI1qv-+Sk-XZBuLO0)<@#aYcHMw)@t`v!l+m{`#keSx!QZOFC@OIssQ>O*} zoRh&d5s1#hDsFEHjnO(*{viar@WqlJxtA>7(*1bvvgFNpXxt}EANEvEgLb3thmf=f zr~Wq!bJ!Q7SXOYBx!Y3l?q|$SSun82_aUFl@j9tjqJdBW4_V3SI;%Na4=9wB_*##) zz*twjx!IHm6IuNp2~N)cA^~=&=^lT)o$w1Z2x?{|eWCD10Y-j!VG2Z6>Lq;|Ta@o@ z1*;^Q`i?YNpV3`qWkBVr+IE&QjLdJ@$A|N&Z(f~7iv0a&RUP#3d*c}{*h&;V!bV`9 zF+N?xuH;^nFF5P#(SHSX|`a#ZX07zMXF4p2h60k!Gxmp}@@V@E^d@FXeoS=4zm15-JxBq#6r!Gl zi9Sh{Ku7h5KTL^!q7!Vy!x2%QKP*J#xCBo zgbWAtxcv5_oW>mPqUSCm60Js)z<5fVnv7C*%$qdSv^&UUexNmkNj+B>s<$VKIeo`d zC=iTzd|`B^ew zEjluE>3Rr=^Zy5xc0vE0|b!-ocRt1F;*;TwyzmYaJcl`f{J@Uq^mHr@|l#RX~H zQI6k+BV&cK@LTx=D}4a}9WI5iY@L5UvU0Sx3syHoCC1((7or?Y&+pzi_v%pf1}AL( zg;|j{zh%C?r-&MrC!&zOT3;aT>ip)Se#`+@e48fj_souOG!+Baim@uuJrY`;A$~~n zI;e7K1CIhX!&=JCE2X7LLGUYQ53=^yZ~>^JgO#Kw5^^xlBaTjkHNXF5 zPXeH~DuK8^`{`Tx)dt|Gwm34&?;9!}Sq!wNfCsnXgSGN`nfVgiN0pE_BS?Sp?-#D3 z{r|}X*Kj0-U=J%Fin6J6H1!hUwlwiPi?_62;vo z7uIz*b9CRgtpGy>jy@8b2s7*h0+yzA^zb|(JC@BL{xyDN5o@;Kd%r5VeU6q#>kONm zzch{$_0;#>8cn#P>2w8ya+C&KaH!1#yMyGY^`BnDYVn)KM|Wsn#4RLkF0gSa!zE3> zo;{N~WUsum7E|vkb?6n%qA;(x_VTSc_ zEo=>CAEgal*zfk7?aPp4Xgf%1ui^?2DtZv`yQUjHdilp=``{Hu5)u|b0~h;B&yrpq zFSV#K`;sRSPZIuGEQFH)4wx@@2s93oyh8Z`XZ6X?BGT8wvNll$ezi5&Fqs6df2^v0 zFR(0Ca8!zHE~l8@bH+Z0B6L@Rhwh04S|`#cy_Vp{5we8^kL`p&M3mI>ZrSCANLGV) z=6|h=FrH+o!{<19Qz20@NUVOP=5;Q+p+p0m8;1L7ZDUPO9xGBhJ-ppef#7+7Hd#yKQ{BUtOnnWZ!)QiuhMGg{qG#`!DddX zoCzJy9f!@^Xbvtp1ojW^env5WNU#BL!jt84q-r5iCY7MyQ-c1x59oCg+Ctg7LBsk}{Ako@Bt6?mvQFG_#gnoC|F226^k9S9R(K~N)*UAD;iYVVVgIGtE%P`I!Mcl`b>DiEN-5UKf zodqhR^xB~B_z9cQ@I1`UM3pHcPSb35*Napx1s|S#bN=R15)B@Mq(t$zV+w<|gZE7C z#>U2POtM0Pf9{)HEn(nN%rhMf!FqKy_a!l*BEViJK(i38)Ja1@5SFQUHvesJw0Ptn z#?+}Vzxb|MD(oZ^_^H3)76{>NL+WyoEC^i>Sfm$4!{C2di0~dG8kk_8Q7uCN_nuul zU3m1`3T`7)u*zjK^5PgduZOIy_WO806PSt|gzXyy6;>=<+-Bp}$Ui z_Ke4z`1gp8hA<0gUyl@&<17No;4o}Hk5-_5fFp5>xsi5hErtl1zT)lr>-v-KboOrc zPA=-vKXI`#+%H3lZ$xo)qmK+mb$?*pE1oN5!CSASkdn+J;zlTVvkNSFKI#1@R0hyf zlnkLsn!}b}B&Y$#4VM%afhF!w{#`Dr59#B^$~fCcj~8Tny2DG5oe#RVzVE+N3C-QP z&6;~?$mBfH1_h2~BpvS@@J~owEekPR`@|AfKYCYTv%(eqzyOjN*AeQwsl=dzA^6{) zSjHQnF$8&rb%g}~tC|G0?QVzJ-zmTF6UUWuzdm_H1nyU@5)RRvI5}cg!j<*9bIvFL zW-n(^piDetCrAG}67SED-rDy3b!#AqdqV6*<_lz>+LQ>jLBQKOYgT+#gKA;~FoOp` z=JYMopND@!021!lBbdU7Hk$pPZn(ODDgx-$EfL z-!n}7)ec11F~VtD6hcpAR`>wZBc&)d;}9QIsk2=TgpWR&$ZiNHb{z5nG4>j4Hqmqb z@TX=a=nBEqmpBnZchYuSuyjt{trlEE_C4^6v)K?qjv!^g6#44B_W~?ql}75583zDZ z$|$OF_Sdx-#lC$!g$YL?o2%w@OvoVL+pTnIi7J4lsFv_TD|MO3etC!J=wJBb;U~BKR4ZT7{&%%xD$nfsq;ftT@aTNTu!Tx zT&}9$6EKjX^!N2Zs`USW0w@zbnD#W(PFQVD{6Eu%-|yr*vb2Xac^hy5sU7qGkMlwP zevuJ5YmxrPZ?CLHd>^HssOX+eDnFcq>z$K}1YDkn+(4NSKZUw4s)!>$tql!Hk-J|+ImJ@}%z#=Ll9ZAm zN6QlALe8D~v%p1I>A2_2T07=aKJd#{+dW1b>M7sptEfqYbBnA@(Jl27!41+zE&QrW`b3Op157u@% zkHaubI@}*_`H-9Q1x2L7qO@3_RWxp)8~v}54lPU6?;%bmLLSqu?khqr80s+l5-CG2 ziS*2ViuxB}00eVu&&Uy*A&voOIjhUf))#vYrx!H%14_vDrYZ(D6Eq`!$Pa|y!H16< z;N)4Emvq-W7eiekavi>OR6yk%EPP_LbrLZK|GAp`QR3joVLBhLQ9~^|DJC5n%IAGR zwLKINf^R}HV0CHrW?W-LoAcbQ@){`UxUSJrK?(xIg)YB}(|i%po7IA(XhK4EzD9M& zWiXsHO(Wj|B`FvOji&kbPQkaxO^45nk8jCu>NL(B(4=mKkMGF+FQTN#gRlVPp8bvh z46P;Np7ZaeF0JdIl4mM(jM=avs(6E9nAcrSqzM85)E#|doQWe1k2$NC%{@r(hq>e) zGg$iQ(jjJiSrrI)BR3A($!+d9>b`4BBR1_sz#*I+C8edMSc#ZU{iw7nv@~(GyCGA) zpXpRH#(rrfaOd#l$%^!QRy!WNgO%~9N#ZC3Q!>0Y1!lX{oc@}GWpR@YVwR+cF$UUR5#&>;MpV$$uRQ(KpmL;2Ky@Z+z=VFwH zdB{klUC4317K+uf`go^XiIjbOeE_B}V{B0R@tC}wd$b3zIg4)i)i#r(sPaBf1xiv{ z&6>t{&O!niF0|kL?1br4JDHKh*aNlcB1 z?YR!=xr}#lB*rJY!MpZWQ!$%D@0p+WY61=~Vh!&FsxM zfG{w&;~*Si|G0WnEC>HldlSY_-Dr9K&Pgvgg4;PgZ>^UI&G7dR@E_advq^Y3iIVDk z;6U=B+m?gWtLHyG9*f*}*0!CT7H-eX9A`-ZO9@#3XYCV~I!O_3a+0JXV{dHEyMt1x z2Sazmll<=PZj&1|F9M=xgM73z|A2by$LaABaFLEWCnU~!g1G;^8hDc*aO9`N`fJHC z@XN=5kY!ZBTxuVex4Y>(%47#;O3 z@?b_%M!Fg+HeCXAu3lUtQ9**5&fC4sf}X?XiO1Qym8&M3f*_(*xI*n_Oro6>2<6~d zA>`tS8hz?@`GO6=^X0)nm`1FJU}%_7g^i7E{`Kuj z8*W6HFjH}P06;fjdDFrRalSjX?R~s>(b_qDmX{cEzo`+Uqk7@yl2{=D-f*39*;^yN z+qiN_D!_e)wDvG~@YX71NdT^9b$G3pricqu)6;cu&`wFkTusRS{nxKE@SR$V~?G}tNHn*ZD(JqTD zJ@bZ1B@k?Ba-5u-RDf;>HW~?__DyeC;rn364{LPPH^bRJJ^RMwb4KnX`&&4;)u|E- zUB3RIC}9e%#&|WqHzLi3rm6Z8y2Bn_x5l#0;K@IX|LZ?faB`x=;3JQM026rTB3&&{ zmBQL)FGM_4?{rAj%)sbka9mhz2qpB_Pj7a-JyH}5U@xZvhUu}(U@eUuLs@UWsDq{)iyL}g95U|6%}f^sFg6*L{UY7;6FUO-l%6C=D4{>o{epKtTWx_ z^@Z=*3cgdz#G?(1hf_rTS{KwLg8zcK!B4eIHIBtRPu9kXTbzSVA2F!rZ!c;>g07IL zqbpZp;jb{7p>rw}@aKSFqf^rBwJh&)JwzU7ybL)v-db-QAS}@fm5Lb%7R9#)8)ujX z--LF0wVwPHa3C`2w4bT>JKOqAnJpC(%?>|@ROEPAne8|@xEv>4;Vpij@*JH(!NY)} z)8}E`B*Cu$HZCNJ+`hnJ+RYYm3My*qm$#qC&fk0wGzkyX3l6@UAHF%WNuUyOKdGf~ zf4|z}v_Ja*MnHNfmK*H|h)(dPk(X44Hk@1rx0CwZD5-e{E6AWEYudxfY*b&m&_V)$ zfq{mL%DmKs8V1d%RAb(>O<9VnMsBZ(n?>7*i;dmLW!&Cq^SEZFi|Cb(N?Jr>!Uj;{ z1X%cA`)f6i1kU6iCqiH_jn?TO%(3}jXv(#T1yTXt_tPgQettLB(6#QMp9%@o)XA%b z6jFiLCuORK7ZU9+iCPb>gyGU9%u z*ZsVFS}a3qS311Dq|=z^=223Zg{li#M-{ul)l&yHwR|Tiq+_9j$TDLZd`lanY_+^> zC-iBVymSoY(s6w4`myIJ00S^zfq{}|QwZFqp8B4r-JBxBV}(9t^tPqIPkQT*cwWKQ z@4U1vy0R>iwf4BqEn1FVwOmuR)jy{wB@wfjE#LF%G1o1vWyiXAC z6U6%j@jgMkPY~}D#QOyCK0&-s5bqPj`vmbmLA*~8?-RuP1o1vWyiXAC6U6%j@jgMk zPY~}D#QOyCK0&-s5bqPj`vmbmLA*~8?-RuP1o1vWyiXAC6U6%j@jgMkPY~}D#QOyC zK0&-s5bqPj`vmbmLA*~8?-RuP1o1vWyiXAC6U6%j@jgMkPY~}D#QOyCK0&-s5bqPj z`vmbmLA*~8?-RuP1o1vWyiXAC6U6%j@jgMkPZ00L6U6%j@jgMkPY~}D#QOyCK0&-s z5bqPj`vmbmLA*~8?-RuP1o1vWy#GIjc=-VUAexMpf^0GXg_Jss28Eh50sv&{JURx4I+b!7(uaKyivZIHmZ1c|dgdfB!kKBMO1i1q=nCGPWN? zoT46dFhu|Wq!Cmoq$o)M5dhTd$H#~VROALBp@0y?Jp%Fg_tGKxK+5dW45WXeMJ2X~9c6Y}5k)fobrcrYMcRx2g1Nsow_LI3U3bt^QvfJo{~OM6jEVLG5!e3(huWa2`0#e||G?R4YiVoU zK05zDaE?Dv|KIVvwUc_p#BBIIQeRi&;`tlr8UIs#@x}9}FPWHF7^f^uXU{NR4#6W7 z#tX9-W1%pW_VI5p3Qrx(UcAxKVYI7z3K=j?O6t$Gs5d`ve){s`r>FQXt25(IKR@;7 zVaTZ~XaBu)<@uAjTH+CDHI-h8mhQJa-uBw_-w9qt`R(8-?L1yE`EfgB!;y;BxOtK4 z|33b=1pa?7fu<+dCaNzLhM1rBJndGylJu}oaS!!tuWqaKm#+Lu!Z2}X+V7TjM5y4| zI-pERK^O)mps48W$Og4%qyx-5qbySO6fOjmv{qpY+Y5cC$X`bo%U%+^F!M*klN|}k zxlXfMS1^#q55AmdxBA0l()2GZ{hrPV=;4{@f0OUMxoM)_kWh$aKdM6{T#(#sN8KHp z0WWeJ`fsOD8?GG`@o*;j5-+PPvxWBcu1T=~womr)X0UT6(uHzko2Qz+gV4j!z zoL_j*0hc>W^(kD<9F}GhY45 z2>6cLT4rFWB)|DI>oq5CCNZVX>bZa|;Xb8t7=+my1*SSv-BX+Q_>*Zn0dy(~LpuB#kf&M#{m-ml5po9vZZ_i@xpKvlqp6 zoTIRzn3b{Ja3j|Jn$xw3=cl`(L*+7O1g1wXV|zV3aCx%xK?jNOpk-nA>E}J4W|jY< zu6LBYzWYLDgWfW687<~JAllvu;#|!c(CW)vSYcL5> zHeyEee6IgOY{zc^uum35@a-!X*V4WEb^`OSYDteU<_3G-Ry=~b9LFwMvmk^~qzOgs z2HS00ZWQ_N{6w>h7WotyXBpcVG1F82kvvlxxs!u`wt7ti#!qfNJMmEfsanr8fxnyL z^N(P@{K@vOep(+GL!ZYS8Hw+->WSQNWk}L2_xyXQOPDe}4K(OF@ads*US>dHqw&C$ zWd7ezpN`M+qjFn=7m4krBw7RmjJ;2Ra!)n;?OdPlf7>>^*A%YGKd!<(t+fPeLbeKo zY{`m%***XKm7b_&UA(8!tQ}CU5vU@1EL!eG)$>?_rrxabI~Fw_U!D0Y!e}uYZWdWg z`Cd42E^v>%Uk)2%;p>rXTIbA|2@$!ng0R|~r5_MNqBO^Rb`PlmNQisH>W`eiou8OH zKM~%fv82=AZ|T)5jRgPU%FF_WCd~1>y1O5_RWT&k>jhoQ@Uy;z2st?>#n3gDmZ8rS z@$LtwetZ92OZB-dzW?I#JTw-__=SUMKb^HBRSm|#jp^;bSLcI%UaQA?(GW4}$oVz* z>f`FhBMCLet)fX1sfxO+i59)&{%IE${Q(54&j!*5B2*3!26q-GYP%!Ke$P5{Gfm(3 zVI;#N3UdglP=A#VAg-m+%*ZnKu6NB(|2>lOSbSv{W<7d>=F|lDzsRo4RkMdkq_$T= zPhLk~*(on;2{%S<(fj1c^jG4$jv5!II-zihk1{iVr*7N5EZJvMOFBc>@-lp<=-eP) z>INKgnCC>oqi$9||0~15JeImka**u_3F0i2?uE55M1RwmlRPE!_S|nclT6+h@eZs2L}?pXLlk-XPgIdEq7?+k3aHlV@c5!7%G zRtx0eX5EYQPH*vkJTP@eOcgJ)UbVJ3vO{tN9#2Lu^;G{Gak+FLd!-a*|-n&@Kjajo*g}S<&)<;+P_T1A+E-QLee-%1H^YxXR*_Lkm$Wm(T;=Cb)p zALEWbIYLIU(IB$AG&t`Z`B=BwSn&u2`3m~If-o&8DX*ls>sWEeYMyYnxy$q&>@Cys zJa)>dm5E#Sxf+&5AJ^d3i*gJqI>p=o<3-Jqot z)a03Wz#4kmJYlruV|>XQVc_pi-!9aQm5m=DOFqASSDCXraHIW6`c;c)9c!nHze0YB zD$$qk93(%eIXAVBSfJ@CcL)3})VUD)F-xqB0f4r_%TAmwX!)_ox#`7J6c~s2CO(1+ z=p(Rr)~m@@;KgucbUYpVOq5QhIhRcF%IhO5OGjOt4!+SQuvxndyU3jZmpgv^Kcn^ z$jsTQ-o%N~_zj3&4hSn^?MoLc4B#e4Rn0P|WNs`jLkXj%&`$Ci0_jI<>4bW!zisYw zUWs(iCh-S8=zfNEy^7`~H=Geog?71X_NU(=-dlB?(QmgH@}%Z8Bs7@3vG_&nV3*$h zY}K#A5>a-IENYun5U&)XT!7bWozbUNz&ZxrmFagi{vaP>y&a{C?A)ezd8O37kWo-P#l1z=D?RBkr~G?q4d4 zF=5=JeX%jg%P%7&^4GlQ{&9Wv%9BnvSSbQ=V5KkG2_j&BryL!hANhmUaRVkg$?T0kJuJ&c#Wo=`ZtZcYIzqP;)XM zYxrZO{@9F@F`Lh#=6v)AYs7MiKhq?i7?|uke4d#u6yGa5PaR#HlVn*PDYIfKmgsxG zkKXSRb9oEtG;46!lj!0?rzDymbV4!*t4@e9z#wKJv`-^GB|X zgY}%K2E*nj(wzmLE+?nvQHpT2iDXS)%Tw#GvN4N(fwb3`%I6lm=7+&P?|WT)N#&;@ zpIDT$4_qFO?DnDOC}NoPih~$gWXdfHr90{=ipIc7&{)#;^P$aX2%(Lf$3X3G=?6E~ zqCZ5mx&KBTstHjBvRTF?S@Mk^PoGvb_#*y}E(am6Bm)5um!;z(Ag%?L$Qk;U`Q5*s z`nTLVwiogXjUePEvHB&F?!DRde=Z#*7(xbpooQ2YFC)5>C2pW~TO92 zRS;M8?a?0RS6WUEj)$keZDVOyDefr~ZJb#2(EHu9_pHs4T^Y~TA+tIz_sOrt+h&kj z*~n$rjCZykTNZtmJK`v(|B2Hp#PxkSsxxWjk#0ztb4-+11v#Si+rN;3@PlNj^WUU$ z6;>eZ4@T5N4oQX$ST@RxkHsi$lnS^;{PLDD4>1jOI&3<&X4oUnn@6w}4RyP!E zyR{oY?%Wvhyq{zMSqfw4J^q3K^9mp1o;6X&MqWZ*>rt|_5>^&NEf6y{%_jPiAsnBPlZSn|fi9+6{Lg^T3lAAl^@`7rR9O zrMIH`V+G_R6>L80)cxeNR)pDhs#!#31ZzCyS$ZQ>%?Sw^zNshS^bJ(|`y8;<(518d zqQ^_7z{~ARxpHowOoLiiH^+r*9K@ukQ)WMj|Yo}1{ zaHkU7RR`i@Ewd+V=JGEmmRnci4tB5Tlfw2NdrNcZQsg(n6HbU&IxyC2jwJ2-&TwdX zX8Hhv0_?8RT&O!q$ctRu%xb)+=Zi|Ln0k)#e*cq6{V95qSL1p(8nA}&n|$s1GtB7u zkSHH}!s(Q_mgWn3@4tqb-UOP3>Uo5>Nv8g!g;T?#Y7EI<#UD8@`1`6ueC`p`Ag&uE zVFCXH*->M1iKH5&*I;)#pEl-LzG0JLck2}(N;Ko$9zhRR%zj+8+@CR2Gg63!tK4p& zNW^^DM6_qUv3o98;KDT~)u8o{3-xa9h1#6>A8n%Op6Mh+-vp_M+&ozQH3`e%t=Pt5-QT(RXiGO-Nk;uw%h%egs}sFRN&&x=ljt$@6;sEX{7>3je|{x zYeJEu2)vyhZh53Xe$^PF)=jts$v2$nCW|^ssFdU4EXRNwOAgh`LhY#-;MY+-AGu9^ zE9h&@hjD8mQymT$0md$s!B5|B8rj}HSW8z-ou`!e7{cTU%7?`dg)EjT8y}IJvhiXj zZk=ufQ&<5Z(Gu5N9$7?t+xwF4(4pu0=*`%?6Sc79jDWBEDCLw1@Txa)){m|kgepSw zz5hpM_M$!?+qzHSk`Hp8d4MwLOD!xarBrzJ$!l;%^FL=JqVkzYH-|YgWF%Bu-d-a| z{@V)iI3p?W&#hI7Hali^Zk^f=HU52J(R9y3POp9B~ri^04JHC2tp_M7r?tkw0a2T$-4IOjWU}Ppu1)cPtT(x=#A$ZtQTk7oj zyIw1K6!Zp=S1h^_OJ*4nsYU2qPNG#J25u&n|2wn064w3zf#n zil^1zN|9N*#6F6gbr4m*Z4-*I)hZHO z`Uy5qcqQ84t0K{1A1_}2T0SJ~g-S@+S2ZK4k(Z5`0-hJytlRoibh;5x5}+nD3_Ekr1x_7IgC=p)*)}>tAsDSsgVU)^*H0 z{AEjj`Xnq9`-Jb$9K8xB4XuqcREkuYydaoT^;_?j4|zT~yr%1ypD zp6zB%`&?wwjktjS>O1Ayr{^|UKy}bLgUGkr=e;V`*n=#93Ze^}sG=^JYSaX+5@_%5 z{1+_i4$%iidIdt2)J?}-#gofP5YEO)x%C!D9=}~+RRM(^5%m04o!0&_^&y9kdoO7e zv?0Bk$@}_>bxMrZ0{F$mKnX7tR{o7_*q4sp+u2{vLl?%sL*>%mzCQt9k+J(|92Rf_ zZ{tIOvxzC9z@4}KoS%W0%ranCox(v!o2OOH4W*D6M%w9W<^7(A{nSn~@#A4wGI!kd zSU}SuOqt)z=no|-L<7JJHK?-Dh=s|iw3*+69wzd4irxfRK(2+|h-vq{f*qgnS1vfY z)7bmx3y}J0HRz7>0ialED7Axh_4`+l+0=d0SLN~KZiT^Q=`CRUQ=g|19uM=i^TrT8 zr21~%H6kQOD*Q?C5b13JoC}Ni9^f5i@C_;?>hZlb%l8HP1-Ub={pla=2vkSyyaS|j zGYvZ|w?m3oJKXxyj($rXjYxkw;t%V5AgWCMzJB_1Zh5OID_uQk_SiygW?1O-b{jiW z$%`sGQ%jFFz1C}s***>_=HeV6M&8}8lOYqA%_hWaepa6zXWIhd6BBTgDRa`-6&kvY zSY>6Q?YUuGBHFPns()dL0HZa)aWb_?@ICD-oou|_33wp0(JCRVL zGUNyDYz)!a0=QJ-#VUe$9vTIVp}{w#IwP$|yz)}a4<6VD+$3MaOg}piotHlV1j<7^ zD`;KRuiOIA{fu~oT!F5`42t$LhWZ0vw3s{^nLpncV*Ix$QC;|*CAVA9W!%ijebLM` zAn!pm<-1}7UUlI+sg9`EcvOiDcFJF0y;FQ#au3Z7V0x6Cx}H65fr%wcTKFOhB%%Sombm1B4J1?NF|Xz z;ip4}cPjB-8jt0u-~UACHLK-d^r#DtqNO`MNXxq*5$L2NL3UAW%l$Lf?yFi(PSB7> zdWbB4ojIhrELvzY*?(h%f7tmAvLa`~B7OcC*gZ#d)uEl6xD%Do2c+0d zH1~mp#K|8Wf`ORxnDh7;h+;IH|J|Iru|SuCPn zKD`!zcW7ZS{#N7d=6)7U*&Nx4eD@(y1P_|%KaiIF-X?cndqbk<66>tVavq|~;Hco; zFESBQiv$N*fm*`iOIl4_#S-SXjQD_r8sqFPAm?E)T;*g#4{D-xIzZij3!%<@aeGFG=j_?P9^(m2!NY zdi?wUs0~KDsYBqL$eB!`#w3tRsfMJ_P@rMY-5w*_kWg)maP3G)xZ5qDif%!qcV_fV zsTYux1htgiylLKto?d96tv}^10h$?Pur5h9@p6NL@Lp;ooVF3V&^)IuL+94kPkZBr}v} zVA;grUK_QrITD6Yh$=Mbw>B3`_W#JRJRKSbXG4eNIiHOjYtd(SQQLD$Z#L@Rk-PB) z9F=&(>|R0oy;UNGTo;;`)Nznsh9BOuqH^^31>wSksHvAuB%7W+>DBt>>>9Yp@*!CD zuzByeaql=Uk6$H7evj5I0Q;6|Zoi&KUDkVIn>ey#=+O~|43a6Tr2f);Nw1j8LB&y$ z8-KYdua*01(JJ6MmRnwtk9~{19hB|aT1fbe9}5nkeoXm-@~-bHl2Dn1E{-FbqTmJB zhw)pllaew&Y3+>1W{_XgLsj*`&$Ba*R+I^GmtCsuvL_|$efn~ z9H=6x2x>3t6q$4A-$2=QN!92Cz4e`Rvc`=dJzhg{oyHiBwcKVX# zEqxnfs%Oa!07DeK`aYE2&b+-${ETf1dM${9scPBU@cp)~mc55DOE zy}Rz}EzJUdwck{o(=@lx$($#btYa#ckpwTX&F?AJUZjRps93v>j`5Z`m9V|9_^ryC zn%Xed&$Xi_(TMR>q$D9U)0K>aou!XD6hyay?-%|B`EKL@(+NcoH|>@g_OOx0-}JkB z{#|d^GN3e0^7wKPb9xU)_2v13@R~TDId?oC-YSV|cp{Ez9K?@Pouy>HLhr3UJ6z%4 zHKK8KS{wz>)#NR4*>$M^ZZ{s>pU7R}VpY6~_P;vs&*Ng~5g8&7HUDO?pe9g+ZX_U0 zJB{%~g^j_cItTb+9vRQg66oaG%y9OQ6D+3m?$8vpRZup9MOVCY> zeDEnHwCM4!xT#v1Y+unE*(=0JIS-osCUohQV`lxo=v@|mB`}Vfr@Zr)TDxGZ0?b`P zQkZ;#26%{=aV45Rb#wl1z=GYc=~WHOX|;>?g_Tsp#7U(S6vf>-Y2&%#H>mY))lu%6 z%P;W~+Hm4b6v1zm*HLN9zdvx!Kh8J@VM^P8Nklpr1DI0e2JJkw*1BQo0h_YNVNk6H zKsDKF+wYvs+&1R4(^0>}zho5CbyrPZSkNRa&DcNAX%WV)K7e(%D}+cPLZoAZR5rU| z`L73^ETiU<9fj6R_t`gC{!yVrDa~pTlHx)%))A-wej;`WwjWcArB*(vpBHu~rX zcd$Bj6y06Dd<3o6oS%HNW9J%*ccP{5up)1+y2ws?rKWLcH3LXU{kL*{aW8IGCr z&)kN4qp{J(a&QYu?Y11;06U9(qfwo9;EYm%#S5UR-8={F=t1h7=~@B7tyho!v>Q@y zS(I_Cum>pxkgv$<&FxaRdg-XfB$?RyZM3&Y-%3L!x>)ZO5NRAK;abs+zMefGpb|uS z0SFk378FT#Qm8IAvdCd)y8SHPo9X}voXqA#cy?ijbd9y|HPsqLzflH)%y9agjn@2_ zKjZsiFboo3DmWlx%Oue;*GcmO7) zOzMsOU~1{17xfwGfv$vULA)hJBK6l{{5C#A^tM8f1+AnADD77lNdWpDI{B>+dg_wNZyQGfQ~p7k0uX6U4suhNn8P7V!mBfHWYIKx z8JDt9_OF_N#p={HwNfR4EbwcToET^g*&cbPtU0fITczT)hDOj~j*u^I%K$E1OdWk! z8`$kat+d=1J_BB=wcIDEZkR@@-R2~{go`VjdReiaP4n6rsFFgANr_DGJWh)jGkC=E%Ib;vj6-}JA&ao zjR+dOFR`_AwL-C-`_tC70oT566WhpWK~%oXVGclU!}%imN<`yf++6umrTm;fCaL^Z zn1si=me7WPm~|R9gKbNpO`LFG3l{34w(gVquojzR-FlYP@7-Sz<%5n+I(t};z*m5_ zFQp^hn3IobU$vB_1{s_xK=B)OpFqNuYH0Mm%uEK!q#j3(ulj#vHQbC`LYY zLs93-K06mkx6aWF`!PkqRDS7QRV|-Bf zE@&2GC*Eafia}`88cl!sglPkn;iR1&J)tZ^@`hcl$^dpR!6S>6Juy>9L3)We;6>9Nh;;L(jywzb^_lM50jU6hXtj(%uPKFF6yDILB<$1dMLI@e za%Zdn=ylf_lK_k&hcc~0m%A6vi5IEo5Y*INY^#saH5=D@Biw)Wx*~00HXYHZ6B}8c zEElVz(0&J6ww^`08L#y~#`bZzL441c^BXVOa1l-;Ica;+P9G9=_^ok3_R!|(=ugKj zX)GSQXV$LHF}ctgzd}-mmD1sc(*+<>hyN zI{wj0J5~fJskFw;dVS#bafxJvXN6-VU&RKsZdaZ-^YZbT-3c(*umtT-;N!H#4iX>8 z**+PJz9Iy|*l^{@!#S&7KGV~l)Vd)5(~m#ERy8N@GW)%T0~_Vi>2Oi_AxsSrBfIgo zHB@s?$qc^bi<=OM#^`xYmOY-`EuWQyblrwlNBe=`oMQ#kd7fZus(bHla7vVx+qOPY zL`ddNV~)V2T@8GTLFUf4-({4Sy&9;v#D?Fs$m!uG3mT7Q@1WhZHs^Ja;RJCr?Lo!iP=?)H|}(9^d!r6Yg5)4ro{ z+g#?p{>BJyBZoG6^7*txux3NhU}T}c?pom1lxaVpXz9|h1K2jy_zzVhgh!6t6YG_ ze?B<9UI18GKgYbf@75xDFF4Yp_+U2znV+3Qu^mtjh>HQ&~SRH-mQDCP&3iL30|UZ7lH zY$GR@*m%tIX5GW8;Ti0*o=(=#C@%0WdOY;TTPp6yZ>|F9tN8++I5o707jEFYVZX}p zHoK>0hOAHH>+a1J;iC7XF@ilQzG)5>{1$r9Ekb4`uHFS;U{YkMj0Fj1Rp&SOX` zbEw`*iQKxx){gz77H*K%^)M$Hj!8)dncYy)Km4)&R2dLZ)}g<_u>X0aGyFfBQtnjP zmi~i?qB1||2?^^ zUT=dtrIWhvH=k1Hgt7_9cs~P>zqNJX6rJQLpG`Ou${_A22VVm{-!|m4S&1(pLeWJQ-r7**G@XgGS z;?Rlo&}VPIWe)3J9%P4$xm0rE?dPCx|AzM<*=`~R zuhB!|S$O@JTJ*soHz(edu^pb694@@g%gCL6gl)g+E1}@jyQ+ZyL$PJ(t?Jm2uq8b~ zrveQQ1%<;b(M+OiCI=sFIXo#~IZH7K6r3d#!}SfH<;X8t&!I%iDH+l774R6m%-Mh` z+GxV=fhR2iX^gmux!|_pj>QKGcVrC4{YyiIdV#cEi{Z0U)0;`BGVZVOSh((HqpRRyAT zuLezEF)MvE+8&+e_25wq!TA|yR&*0>A zeK2hGHmF7St355R(5xj_Ba^qVqm+X{n9HclK zljb?~x55*GKbH%F+YXQx)m@~BudYu{s&AnQ&g;(2;!5<%emtwY|Cj)#VzvJO7$e^< zg)_v;21{)}=C{3&dMv2mo+$|97t*SKrnp8xjD zY46ulWOoqrf;u;Jd#iDGOOV&}?V5*A!|w7Y!$^-w@@D!}=l4$~8(nfD?GAXC9MD$@ zXzynC@lY**ksbmxW^U^4YMTk|w}><SBE(Adu{nE5c z4JP+lcZx}8B`QpOd}>-akqsV^T-3F^wqN1I7DBRDtj-oqjxpP5369^nIO zptnO$+*9*NI!r|XSMfpMUgg2Lsy+%Es2edH^BHG@4?n+-vOhVjPTj)$HEjI^IBbku z&HL@|0$vXw8xCmpJYcIyLL<@R5#~kDaQYa7mKzctIom4I)2|rncYf3QV^!(L@5<~g zKP5A^0~?krvGMeR4Y3birA*H8Hyy8ENq8j^a_ADptQ+($j!VEO0PMsmhz@{=FKb-S z$Q$NDbs4nZ1dL#tlU0WLyTcLKq`wxNf${cUu(OP}(qoBlX8U1)yHejLA|vJsrWe>! z$5HmSo!pH-TNCGkn=YHmZRfaMwX;15`}SkmOA~>6bql@Q4!#gHo}B|2s ziYUzpSf)6;Mea5?tgj`o8R>6E1!zhV7PM{vA0mP5s0Z_>%#PTZRyCaMpxPYhk%U8) zf=2NBD?Tx#*Ac#7@Dq`uLhyg{rx(6gD9-r@X=NPWfo-=Iy`DY8NiY)uys9zR_GWa^ zy75tUvs$yN7IaZFqins5i&L3#Z#l#1lMbfN!urbBKXP#jHrV~&Yv31r#LMl&A{2B{ zTal+F$~!8KSEV#BRNQy4?A(o>^&u&WRC_yIcn^!)wLch^ZQGxyq%M2%DUgR}Bn3Pf z9^}DRVQiRAf~A|Q$^}?T!*E9OIA6^&hSGRC!?H5FFZWVn{ zaPRZF?0t9Y-d~pRdj}3@63i}q_>e)29>qDZ3ZDmPNBStrz1CEgX?t(|Kxz=nd0o*g z86OwBO6#-3M9Arnw?x|}qru6|ozLP08V)9AZ)afC&ak$bnqj(kJvRUrysW(FE7%7- zZN~AI!y#2g{C1n_?%4rTq2NO-}^i9QXKi>IzJjxI2+%t*7+(^Ve9n6i8Q) z;MS|{!2;2O;uUsEY(X!Yxg16Ji}dQHfR*@E|BMaU;AEwhFa+ig`e&;bqNXM2*+LD| zKe1|E1NG+C;p|M;%(&h!;1*j>gqTweJ#Sb3^n7fZrsMJhCvExdhIoLz0Xg~ytD0s0 zA?z_{TmN0K0VXk^a~;RVnK&o4{+%R$8n9WDI_DF`EO;*i^O!TS!9{S`V?A6$&&6fv zm%k-Wq^-Zalob%D1IV~P(faGO;-e)SwRU%=GgyW970Cg~4x&}y)cXtR7|Bg}OTb*P z5-jKozFOUdUoA)JQgR}YXL4@6uftGbx-Dg>vf0bvk`)VsO7uXRTc6{Df+J$)hik9 z+eyN|94QR0E3@=MPur>BK*{EMFDIn_AfDTN)vp2E&dQ-Evl%X^R6%X}tihSsm$uc$ z#d*Q^PPx@}xH$0sk`>_FCx*fv_r3K~7f6O`1xS;f{EQ92Ps~UP@|t%GTj(Hb<~kM? zz%I*i)~N2G(fuz%n&OKMFW^ueI8df@3bnXbjtlEDq1^p+oFIk2oOAaXFIzq4CLqDA zHKdMH!QhZ&CtSV#2psg2Edo7_3tIy43M$cTL^#WdJBnM3A1CMS0lF050?q+R-^#lLfIru|9NZUwkaLPAqsl8N8_Eco)kqB3p5%cHmqXhti3s#fI3c-eci!JcdfOS(&V$yoIa% z{+;iJ{98=xZs=Xezu#AnnEnaySbsX&A<92tf1mGxZ_v{WZry#Q^!pWXb0w#M`zkCX z3X&ech@g~$mucM;-#X27Q1BpnT<%C<0u2mS4vP#wf2zM#>rH%v3oO2ut4r@<40uTq zH2hlhlYQmOqo4|5wZTQxZsXxjy#VU1qQr`|I04=$4xoVxPX9Hdt%~ZGx3BXAJuUyv z)sAfm)b{?91R+Hab%cmRZEeJ`S;_F}TUd*!pQd3)LoP8L6RMgYbnd?H z>w#e*VzSP~45xyYU7ii6)(A~b6QzI+OSsO}hV{;6Q*r5~NKt43W;K8)=5L=*9r>(> z&S4ccA7;;9mFXlUD#>d$*t-Zu%T322EGwypdPWyr7;&c#Lgk5O%XiJY5m5Gp=SnW6 zYfR5q^oJxaT8*(YoiAR>J6A(miiV$p@`UC0#~j$4S&MOy>exlS&)McN3AM9Ft|Vlx zf;}&>$J~*@R~&!3crWIL$J4yjX&k0Mxa@^!z57HK>9U4*VeLgIb%u@4NM^A~p^a0( zGgjf#M5$PC({n)j4FxA;#$=rHh}Ys?-4I*MAzv@&oLzuqTdu$0hUIMzu*-U62%_7N zXO9g66NzwH_j`aQvSmWZV25nP9PvsEDe|%Q`wo#r0f9!6O?-w-_@pa&Lp0biM*4hv zaO*EAUxsls=A3TA#QM|XB$zf_0l)dA~Yw~!3-rTPsHJ+6ixPm<5|zRqj9 z)3qF3D1~o$1d(twR9VpC%e1)_=3~(r$WJk`y=8Zhvb&|iP^QKFF<-J>}q^|!S=FU^-3P-aJT?2WbF$k`pnri*#R(c!F0oZR)C^X<5$Ki z^WXjx8llkVKRLS04&a@$`_sMKOW3#LdKUCngHt5g{)Phiof!5k+cPB{h4EUQd9Mf4B%7?7p613(BG1;Igw1Ds8!~X-Cbu*praq&&(#W`@{R53O;vxIuQX; z<>e;FsfWj9s%WiUtjFxj;q=L&;a50gw%W9iC1YjvoE{$|A~e~qGc-=jn;5^YZkbJkv5v$_KlguBjr$8^sAq8#E ztH5Ve(Ig)CM+6-v>4LRdkpVZzt5rp&>?H)__Xv4=^VNHw-G=j}nysmEVSJ&OGl{QI zm^h_T&9vi1!{OT4nT6FAHdcuPuJOBOl>21gcUrK z$X2&A_X{%^3ZGKzOH0ZVLHkM6qG7vd&s7lRk`{P>=P9IrBREg5RLsBqR9k6EtGa1R z$C=XKSZ=8MGsV~*!%JE9nSDnztNGDRj@%a=h;II)`TLl#SGJe$H|JPA-~r#yxd1cM zU+joLb}tE7EID{F#{NjI&yL71h-?kzHnte0j%3NuAj`qC+yX$A9}e1{A#pLKt)Z9O z&Rn0qxjz!bmCUm&S3|e)J61+S?<9dM$O{9&CDj$coP%vmpMTKPQGU*O3yLXYuL;ro zpGrpPfv6fJ4)%;c|1d|7VMATPKA=ehm(=iKNdfk@nkHUBcf)3Ts1C4_F3`~{|DOjY zU2a@^zH82M?abG#`6_~zgg->sSGMN3742VE^}FNN-I_SrXm^yhpz=`LM*f>knXvoE zI^=<1b}n+Gi}oV-9DKXcPzmw__82tGaA2DksEnlDsIx4xE6J4>z<*{YP~6oavwxJz z_Ed?37IU*FgAq`Pv9|JEA*&-mJ;n-WJX?;%EOGs|^LDtH!yrThd#SEES;ha%B{YnL zAy>?90coug4u^&#cn6`Xo6v7|vMT`R-^N%|%T&0K03#N=(n}EKq@%~b?Eu-(1D<%F z%*CYrwC<|f=4NWS#jijZVWoJ*&W3U3Mm9Cw(z*`c{GSR1j^TvD84 zXKy$6lQ;p37D`uqn0ETLSH}?UuU`7(A@hbT6KY@ENq)nSOAJKx0FDR3{HY23k*j&o z1J63w#Qleo2rh5KLwgJ;S`k23O2e3Y(`MNq*08&2`y0#jNdTnc7|TfyKpoSkZwXC6 z?}RG{Y&#LFoI$g*q;Tgt_u6fS(WDPm6Yq9JB)k#L@Rwf~Hkz0+bFNE;uZ2d0M%;Td zDf19mzP18zRs4o%9C>FoOeU3av8Flch$^crjW#6)Oukdt7+sFY;OV@NXE}p7H}Pjv zeIIlBg9^(4W=(^e1Em#Ggu9@6&vj->+YWzjM3&{9Yiw0hN%?rFk+;$ZIq~+Jl9GZT zGs45c3-1+Gez3MLDO9cO6_w8t0Ba6kg8N)<2KQY#y0r%FNFHzdN28DS7|l9+>_$1* zUD=~>ZY)FZL;6tkV}e4P^Uh8*FGv#f-~R=$#>C+>9=uz)D~qIZ&IYOwlFGK2oAJ_w zOXFigEkh}~Z%!_2jBH^kvnilpWAsZz2~F~tr}daLloAzCHyiAX<*rHslCOD0uHcjJ z0#5+&trZfR<$bkY%UApA87LJIqy6t{5xSf|23%eO5;U7;M@$wuR0|#X2R@y{ozrH^ z`S#W_PzN=6wJ8|0K$amAc(zu6rPT6Hj9o&l2&YF$|3YpMo6rYcJE{b|a~zsd--U9~ zILX`JT5PP}LuViVat4iBkvoB~eiGbQflSnKmQ>oXNmy(OQJc*m27eEnJ*MyN#v{;m z$Dgz^M>9M~+l#`?S&jW9&q923F?6R+ZQJ}yG2i_}1-)8VXPPG3oR(6lX*CqIg6;ot z0PMjVp`svW55s;*kx2~qD_&X^%~bZG8&lfDFr0Nwl^b3EnCpbNZf)y56_ezV-S(AR z;{m4I=3#@&Zv&+SF7oiTVwVZ8o_`~#67T@|32nX9Q{!6l97~G#LxiWS`}CBfC>NxU zZk$HHKk=F|;P)I|zrJD$=WIvXLT~CWL4ycLKiMR**Br02v#4Bg2OFAqlq-@0E;8^Y~_KXtT`?mV8N+p~hAUk%@FO0bq$iZq?hrM7^N zi+=@IuJm8yjqlB*SJ1F7UOzn>U20Z ztoBa5jD9FB0qe;N%iaFS-|l z%hFQkmtgxY#~+IA%P?$BrpGS-h1D& z`)PB|Z)Rui+9{DVZjPldhu{R)&SyV1cqeS1G@q zpIoLAFyrsL3eKr_eLr#D2UzNbP#t_AvJd>-#55HPmZ*tA%5Bq0x#f;~73}|56pCA? zrWf9Bfr?D9nC0n9B29=eY#md&)CySUJ|^I4F2~tDw&O~f{rAsYVl8cJfKM8ZcT|@+ z5#s+!D=t7yBP*yh^q;ioN#bFc zbRQmID%e~(zbG_xKI1LW+`q2++~z?*(Jq21^hnN8G1ix>Qs?Q+vK%;f*u)6I8H(HH zkbUKEFJf;m;CNGX-dOU5BTO`bc?gQnUfygIiQm=$oP|6q6b!1hY~fJ$cTXJkpatLp zfODq*_5W{_WKZp%FY5K_6PSA|LKtOpT#GvzM>>-jc>;we00+7MiZ*9E{E*~E_@)UM zGS!TVU-YoW2pN$yRSrp!Yn>HG4Ax9eN;p72vZL_z#A$?$RMI!@o+eE2#YnUM3bG2D z+y?MT0i$J?kmR3LSox1JGD+`02}>yPHfMUoPET^QN!+>Y>5r?rbmE7tBYU)3tjRrb`Tfmf!8$ZRrV* z2oS~GB=hGG>jqW=%5li`_=Ax3)<)8s5cFwSL=y;S!%W_3-*0cFiC&Fu%+O z@3`RBW0B&j*{6UEIaejzakSg+AQ!`*2hIhDmV>w)vQ~Av8MM^;Fhpim&;5rxX$(KT z%vcG0aRgD%?d@;KaS4mK#TjM(hBztQlit!13httr__pPzPR^ABeNO5j1iFU|dU7Z= z)m*GH6Zd1A4yTVHU3Ie^EzY(fn+AEBs8dYuJ6;{psxO||vy&r;8&nJ)7VE!@D|(8; zsIL zseZ`rVuc_Aygusc8#1CS%XHD1x~qI44EIO)auQ$IVl>4sL^zdt#^k+vEq;($p!daD zAPvUmi;B#;mN|^8JkpbKJ{&QRzMz zEf3T}ybA?29~mwQ!QH$uWysLgj$uNz-{X^ZATT(u;`%>!x^HEZDd*`2-U(SjolgWK z$Y4*YrFy)+YgU)nAAG1U_hkaWJu*UN)94~JF`Mh<>0_jr?XE*MSgA61_E+`HX){kV zCNlM(zi0}#K11b8a??{19&fO#N^bXKXKh2~s{9$&5diPtMHkK11Fv@y6mNXF`)iAb zjwl3#OVhkQe16d)*)Dp${RDOGGq=iG+c%|dM5tMkc*2TQXTIy~sB6;t$n3_kb?NTc zxo08kYMc5QDd5y%LVm0ts%X2AB-?`MJyr^;yj!8A+TB99839^F5wR{Ls41%YC@%0U zka!UQ9X{ASFt?%4@}WzcwZ0d@pT(P53^%R&V~|S7psY8aYTYm!IfmiJAShJ%7VCF`ZU(~|M)N>B&{uFZjs8si)MNw)dx##f%tBdwm`yxQn zA*%&8Xe@t9$WG2IdF>IIkuZjc*A3c8uo43A0(eaIx{c%rW7^YlgGoJ%5; z1JZCf80S!^;;&m2KsC}Sy^Eea+gSvdD^y0&#mDdW?!tz{y!U*%%Dec4LN=)5xm$a@ zcp!x?LOkwmH+eaf{SJ0i3s6|~s=Lhb3gtBWjcs|%JefuN+&ZR}y>UH)S^H~|@e+PT zZZ4i2two5)SeSe_1`F^3w;Z2iH(^HIfb*6{Wmag{{su1xS-IfVFcbYi<5rIlsa=Zd znd%>q@CAvkIFZUX|1K zw*gQazn(B*;i{)dPi`q2Pq1H4l&>nVyobpf;+6Zz&q}TRb66A4AEzCiC~%~jM9ZD3 zU!A*UXGWFPpDSLqENv+fG;SM|g?CN!PoJj|lURbp3cNnfcIxArVd3R!kRlS|vfEXJui^{(Gf`ws%tvD}0Zuf&husv6dp&y~A z=|S(u+MkU-r4}hipW>b-qdK2f)BeIruAPR_Af%NDT=7g%dT*3)>ws$p{i{{Yx+8hOSiP6Y^BIR;@FHbhEA_55c zBA35Cn{8yha!)#wO}o{oC$x6m+c84()<}d3Wc2d98PbHGWCPxTT)wU+@K4)BSB-}j zU(M{R8G5PBKj+;iD{QJ`!6*T|^$pC{vK!~voYFl)OTzVpNQC>UfTATk%GMsemE~<7 z=wp|d)RAtP(I&HkzG(@Clw>)DtS30O5^SN4AyL|J6`&H%kIV9-`{+-fcq&aLzqR&n zMv08vD$+i$;t|MI+a7(TnGR;HB*6^4cDyJLcz2adq=<(eSd3aq*shJP@gGFh8a7aU zFIeY;U+uM#QcDQFrK9}!^)YchEoj}6&D)Hlu=c0Vsk*9omBs+UmW$A@5J*PmZ)Dve zy}4XugS@JMM7ncuDV@qlkTLDju@gmhzal?5;Sip>ev|Z z5ZW| zI%$~Dc`Z2+BuN?y%{Xc)$4?JHiUR*>K;%^m-4-D-p#k3# zqH!-Jz1AHA4uJC5pWd1h&l5rJs;nw+{M^>Z)bsPSjm~?V*CVgJO){SSqP|Uz{3G0N z&v$<5h_0}&Y%Uxq8Q`n#FE2diMnB!wsQ-aXaZfQe{~1q#77Ha_38l-D7Y5G7S<`&B zLVdl*t@zP>F&s4MeB#0{G3gxFvwS+TmHX$m?8CE!!o6@n?N&5sa$7^GZ+@$b4{n_h ze`RM0-A-)2oPp)ymoP*t-+eRrA($ucIdOCvE*POER1A6tm=3z!1L62hDtKOYDB}XW z);#(0+28N{o|gMEKf|r$*U&6Ru3T;(6C00Wk=qf+pa9n1aK$N*4cUj@69?F;gFgon#x0A7VF44L#bztnNkGyhHOiF4?xcxsZSOyNG7 z%A?cRQB&h^-PQp0m7j;V=XwMW$SXI_n}^qO@;RpxIv4v(Ck}UwI|ZiD%q{Q%FF!kI}S33EHl_3m`h;)TfjAtad*8Y|z) znJX)|;2bBB5~j=mTH*)HNez!o;N$NWoAWtxvpO8X?_77;+LIf~hQZy?{&*Odx-fm= z-yN)x6zPrLMSqNAm`l`a-nrZLF=qY$%7#ZM{tRO{5Ug$JYoUV9B~yZCv-6$JA^olo z^^5J-^tyymVG@YUBHfnO^p*X$rR`)gD*DV+7I<>eZWatDwaUZt@ns?;Yv04|Atj}uT@WPB{#HX302Ph$np zRYN(me35-*MB0MvF1vSdB3kMsR3{a}XsQp|EFOiQJ^Xos3gV()esZ66Frey$O20(| zrQapQbMB8!Vh$RpxIXA=E7s~GtH1TjPJLS{?l$NXqaEFDWY#0!wZ{?h6d!{{*q$WvMO2W_Z|JW@|P-bir#>ps{SAV zdDfN0@CLJJZ&>pYycI7KTu&mC%KFZYiI%)Hd!3vU$#SjdK~~j|B!(EYlK#9Z`m>ie z{JqKed$H4Q?!7h{LJX&MU~5R>YB~1s^GYB|qGu_zwAs~v5#VLd6Su;kaNfoa)K8ah z7=PPerzj?c@)(cGz#i>Hn>>;;J5sj;1u&1q+#;$hb^REf&f zB9rI~D#o(1=htR$`hHX~$po()&VVNZx6dk$&CiaYyDm+a^Fae3P^T7=z-k)t4n8v^+CfA2Qy}O1DuZ%Z!wVJlotv$OaIl*-!LF?Wz7IKG z_BbpZm!Z5|kx1>DyS>RI>&=%E`@kU70%Bb@n}XKt2RurjM>^Q2B0LjBInnCXbx9Y2o_Za~C z84J(6@SpN9-jMS#Bd?0ay=%LHrvXo3)8ll;+uO4j6X=67UkQvHPVk)s-8eM=^6E08 z#z^oqy2j%)P2;U7CD)^5uR=a&;Wh^O=$z>IG+_INK_)?UBnQi}Hqzxv-9lI*DIU=h zKKibNVg58ec4ZgXlqPGHJ%JqufR!rd$9nY%pV4B0*R<;|@@ttr zAb4;p@cTBq3qDWRERC)v2y4%2wK|hcz?eYvp&xM8z?+noC+Ir48-`;V>9q%s(PM9?R2RL>0kxCla748Rb zjW~Gics@|b@yOlM@%zW*r|b`yp$=GtkDD%_0%FC2JHJ~Csw>rO>x2Sd?&(x)&GQA9 zS|k_5&km?cYRu@ImqH0k@$P3YN`}1aVHJU6%B1b1L!>k1nE`KF?SG1xO4-}SiSL&ker2_k>%&D&;Xql=4oHN_W-zXx=u$n%MI07MY>qVK1~}bUgI@2 zy(%DLxgK@bF}w=1%*t;WxI$^8dpOqar4Egb{^!m5j*DCBZ3#!ObmSp^A@GcE^mEETB--S=QRbZ7|=XrTL^6u?2l zPKL7;m%)|-rQ%%O%a`|~V#xV2g4d%waxM^~m}qcpbd?)JCtNh^rIkCQJuM%A=`s>n z3F+W$($~BsYLlZre>>RL7mW>b1im(&$kKcHTip3cCNtyMX^P0jQ;|m55dVA^y%XoS zrswrT(dxsIMjjJavMku%6PZO9nVOMH#Ck9ZPaZC8+c)Wx&+Kjz=%cW|QV5{Ak!9HE zZkxEgKr=c2isNk+zUZ>CmL|isvZIf)doak}LhqKP^swt@hT|12gOQBR4;c9EDk1q?U`((}$; z7&aX`)C;Wjn^JW0a*sqn#fm4Asn4?JaAJBN+f@ru)N`-@QzvQ{qT}XhEBrq0UQH_A zC{{5DfGt3V>{o7s4p5vO(4*VgTpfK4x-SRUNgc75V3Y3AYY?P#uz|#(N24&Cs zQ8z0o#_dSE%vD`TKNRUVtpD}>Rhdnbi9nTYN8?@Ac3s>wnr8&}pL(yqI61{qqI3TR z+V&ZYYVcw3Rmpci!N6ORmZ{rD;+c^*v{fbIC^{dqfp@p1fv8g}PmtG($U5@SDjvvm zaezo&UjoDsA}O=vLo8L|Y?k6+z!PwDAa8>*$XF4@xz3@kYhXBcYr7hwH+)%S zXUQc}HZY7>BcA-PHcMDGFWbEImdU(L3Pdb?|2h2i+e}+m5ld)@*Uu&5n z*x?SFFOA@}&BVW$9w;p+RW5jSSnDvjWgc~w9QkW1TT>!@q_0EBw_0UxI3%Qy#>RLx zY#~!_9WF(O?>GDHXX?L}8-$u*`nmt;Q7XcUjM_gX5k#;DsnzG-97!Fn%S< zQ-vr8>#U}MiLT8Os`&Av_B?=KlAG5xP)dHO?0Rz2*zEcH(98CSV$z3icXmsUJ-S;;H-w`;2}v0*`uvD2?W!JFu~q;u zy^pScrBd~@UK)O3xPL57cAp}bMX042v)kZ9{2}~uSAi~=@91i$FJ3kfAp^OS8x%xP zddF1QJf8YPriCYAAmLk7-_w!DjxDdPT6YBQD1;R(ieqMJ_uMxVZT9zXym_hdO^>}h z(~w%{&tzUQQX90!9|Q#&J!G_E)vnu;r!vQ$Lsl*~dq{k$e#ZDAig4LI^}7#FHQnl5 z3SYCh-**vk=?eax3l)RF3k+%I`zE36`>#&Lqzm`E=Ecr`Hc~xS)A#b2ZTx1?zoQC6 zRqNwcO*oU`Z)>LnX zSy;laUs*NIO#O}EGQJSLUPY%s5GPu&2M(RumG5l0A^GYj?Rah(fOp(z^Bxt0l!7z# ztq}L|41mj@-=*1nf?3wCz2v>_l<;-zT3>@Y0`{B8tZ+@c$_&vWeg)>Ya=28wjQ@Ou zs1N2N7LCFWhJhaKJCtfC{(g!(sTN`V5iYrjJuLc<$U>wPs0vnXX~tVNoBh%T)sjpe#`i)0f?nu9=_1It;O7UT$Znme&>={25z4 z*7aNBAheqo*@%nVZ=#}M$``sE7%2&aXh<~B3Oe)FbWvT>WFg64zIg-mdcmv41}PrWlRt;e2dJvPx#G&J$Le_ki^aKBNS z55OgwIof_Abbd*$N|A*Syl0qsNRz2LtpkH%-Yh?t&L*d&28JsJwJ4Bcp|YM*vX(&P zk$?)Y#Xz0L_Eo4zaZkqj?GF5^)N1f{N?chC9nbrDGwPZ@xM0zf7`iB)y@2hRGRV~% z=I}WP8asBxO}-pgf{#ahp^+=-nTn}=9d(i4J!C7T{1=JCiq3XW_tQkS^vNEsr-fwI zlIrtknm@#Z{f-~U9Po%+Y-VwGJ0&RdDq5tg+0VVEk&1bsn(2?pG~czZ+|ZeS7b+`+ zRiGSyohgbLyJsa{u(h5<&gZ<*(3?bXGpK1xe!`XdAb$JW7fSsHh}%rfgWm?8K^1AcTV37MV!@Gmz|$n@074<*ONS9VnVDyR z@;eI4X73$cZibZM8ecp(eS*aJ9U%fR?yWt)p!rPTq640=%-r4POU&Y8>n1;Z&J302 zTdWxEF2P&p0zNqEnnhswNL0*(r+3Yd)sVT3^(w+J}y?Dgl$aucg~a_BRLih8!~0GKn;3n<>B0MJybh}h>oX2(#5srLPp&sa^4Eg!ztmg!1~{U;&xElb_& zQ6EP`cHC^|53(bL_^yG)iM4Ugl^FXSFUl%fGJ~`KSp3OFNhNH71>LVf|drqJG8|#@&<#})5PIvG}*ifuI4(AGK=+Dk%i;R(DwyoVdPwz1EYm z?$MbQWu_s0q1y~)RSp*?5&aHaBnx06)a)UlyJOoHt=Ee&f^cDrA zpEc0jzNc5F`|<8cIm~F#ichE{w4a}FLr&QCTkIfRnW&_yp0sw)&MU~56e>O6Nk6!coc!@!arV#UkN%r*p8WTPWY9#a6UyNJ%qs8sNR+fdhCILOtvT zx2gIT0+l&&eh1~4yCMI$w9Vd)eZ5j6c1Z6fyYmtgi3A{CGp!BD*F;wSt!#FF zhnw=JZnF!eJB#!PNwpEPK{038Oweb@iDfU=V!x;-235bff#%4j^*_<^h>O&F>{qV? z5)HA8B2`7pQ&xiM$XH3uN zmli<8CUh|B5^oQQd)u*J=wy}I)#e@DsxmjkS8OGx|0yYo?sYjVF^t|$R62>BKDA8M ziuOvLihs!k?`@C87_dy%1I-DQ4iUn$J9J;mIt90@I1`Q-z3G+HV>sEjfIQ`4tPhqX-iYNva~|{b=}U}#eS?`~Ha}>~ zzObh;TKmK?#GmF69(PvqgdyK!@ZM0v<1PQ}Yx|0lXy20_K6csL3(btC2{qLtKw3J* z4osX(EGlEq7K?zf|r0GkDEa7bML-{5E;Jy zEm=W$Py(>RNU}Pbjoa@`2=R_ohlo#L+J{PQV5^IT?LJyU2YLU-ZWy)P701b?!jIqZ zpv3#3-N`+<0k0stX@y_LqVFKsOnw-c-FD5oU^sgdbKe!gzjzC<`T9cnx)EM?1BsLMqxs3j+cWp3pPYq34-9a9 ziKp~NTRrkrzM_z29NDapSf)(minypT(2oTpUmuD>O3F?(%FyMcBP<>T!RD}eLs%A; z0E=UgdT@kZc6Xl7oX|+NBR=e4_SebfE1A1e=Pbp5TyNX8+>V@GYr8XnAJ+fkZ(i4W zCaNu`4)qLw?tSHe7$qSIo68q^cO%3iY>cFKWaeMTVlAhfx?sV(CF;l}b8;jBlykZm z3MJ#3d4XNckgCABpQJ|ozgDOYNbHZB#p-<#@H>FbE9Ye)jQLgA(b-G4~^!l!t@y*20Qg-@7`%9)L9JC=$9{{{9vC(`AeS{#7*`UOx7(FEs1~1H zfjw^O0b>o39cpBZ5UKU`0b^tf+#z_gfeUm?e4Q?02}5Gh_rYB|!|sH0eQF2(u~o7{ zM#`lAlQ`s597!io3a}wJqg`?c=%wze{nKr!scwC+Y=~^EYOHeiX6(h|hUi?fx76Kswcc-JF7ZA9-&S^9pNUS@xB~**b68?#S*946-hY8cPp4 z^~ts}^g-1~JSr&Ba z?m*qF1ElV(0=C2dDPx`UiUgq)`b`-GzX?a3JT@hCp|1R^+Fbq}?c(S3V+8-3D5SVO z27Hm>9&CGGs9CacWjx<~6AL8D;%=t~t$Su_oC^7QM8*tcg1RLq;nKuMa*T5COPnVi zi>kaDhaLBzDo)~G49jXlQfJO(+f08fJ@u395Y(ktqt{@TH{}R`R$VhhMUs6$-itOe zM)!Kn-a1p5liy5{!h8)0)elZol=EO}`nD$2{pHnBW)EJoB}wZa&oHP&W74w7#4UjK z;4e1X!!@=7v+EdgA*w)z%@iG&jIRx9$+4&14SzQvjIWc+805;UpD&$#m(;oWG8+=} z{OIzDHno2&3L}-zSE(gl6Lt7e5B8=*pGP*wz!Acy76XO~N$;8XZ6kI!`1 z(trCO9e86*JD=f^q><%}?)S-N+m_o?rmAm@&IVoNGW6u;xB~gQ)To@VK06gs3$dx@ z?vVOUc*dt3xpB8I{wn1a_O~p(Qnxx^e&?(FZ5%1f!Nl^R^7ogh2YuJK)$Yv+WEgnv zZOwYlB%PJD!n|RvM=(sQk!umg1#aejY~E2HuDaMdqP)H6nMPw>$r=(aHP2`xRZ0m`&tU&&c&U1lM4l`0*_GB`JJ_9ONPc z;*>#>&|RscA*@z}h%r|1KXaZK1;-qy*nLS8?xD=BGNkfU;Oek!SEcd15nzU9K{$qu zoHRqO#@I0~F`ItWW&4saH&^G-ZnV0qdH8@+AhYDTTU-I-g7f2JX#|@GZOp(= z*6m5>S$UNgrmAwmEF%oq#*ZHhm+i?D-+(^65@B=zqQg`3f^*ecZd zSJQa{2LUb4oLKG007v0h}4+Xtnxo&IP)%#Gie#|<$lEitMMqj{cO zv*0rBoK7@uuyCON;HIu+%5HC)^0{usbMFg!`Qu7kz$=f&#;?C+z2km9=I1+;l*SmX zEhQr+6h9K(wO5r#Af6Xs5)R_5e}gcIu7I7bWVlq?Q=9&ip|aRU-DCK=4IU#98s)n_ zisqnDLedGNIiiQ9M(=|hB=CwoVM>!bG7wi!{$9C|KH8gd_uM^lNIEf5paa51Cgtk0 z$xmf1WCqE{rgL{kY4Inco>YARD^WSql@Ae>OEgOB?$S@78s2LFrHesq?*RJb2XZ2k z{-#&<$*&H1i`pRogY%3O5MT0_x8;nU2Y`>|Rgt3pZ=v3di@dp9J>uDr9kFGHu@I+7idp=lYH!N{4mG4Immp#x#Fx#1d^3cv^ z<+_^^*g(WOwfd%IgQCv{_h0dq!T6osZ7g-j6tW*}0ws3s(gnJWg8dGJ3TRv)(W;uW zE;kNxKgEs|j&N}A7YronRm^<9M17DB8P1ZIyyydsY}F5dt9B=8V*z*W?Zu8%Ga+lF zp8SfPmV-D1UXvC`u`oI(jgDQ=&YTLiWpYWqn7vWsw*G~Jmkrm8jiBePboumIPnX9k z$0(*}^{R-JCQ^pKW4y4iQvrP~$8ww|DDSaEY7BTIw7}Uq9W?r&`R0n1c|CR~{uVFd zvm+iWg3kkAX6>c`3BCvA$ND(%<)Os!v{y^sG5+w&(tPamJCDmHL9Nx`h#yq`09WxD?KD9qxp62iCrM7K1@>!I=L`#U!t3FUgo zi|60Lwl$9Xzkc1EIG6ZnO3jHZG0hf56kt^nKxjOH^+t_k z0-n*aPu=DbMYs8<{xXZHf4#-fQNN=qkrtMl?kPtjbSOIJDFKqXT#pE`yZAQ&%_N>_ zg#PyqF0@A!D1>R6|m>G9)- zFO)9eCJ`3Qmld8jrH@H_o`wVlFNMf66^cv0NF>QUdASas6%dXDs4AY&-J&12OMhts7Ryd(zH*Nd~?_Ga*egtf;?# z#v3Z|L>d;9RrjEYK1b=7!PT_S9$(m=CTRQ3PP_cbxm<>^x=sIC%|nHX4LN9@F5^6@ z45Hjz;OvTXkBP8QSL1K0w>i{5QGZAlfH}OsW5u}+lMc2wLblr{wG}4zXIwFcy#96R zH%gO@g&!P@*%!jGanRJ|ixLc0hRUt@mqB@@&eYg=O8BIft zuI?jK=cTL&<#8>N$Yjq|W0A98bsc3lYqC=)G^xH&_ShIppRl`uKWP0mWK9(4;)fP9 z+>>m)QM~NM0TF?H3_8n4Q}`OM15h>#)QA7NAzeH>r&{OyK=}H~A?fwv&J1w$(NvQ& zx87$LOzKTPDHwLFip4_x(Wh_xHodRDio}kzm`-zWDmd9m2~XGleZK|xHz;-IZ~cy< zUokWun?YOBSXq6v2%(JNgVmw>^}2059MsLMIMT|2%p z$!X@w`aZntm*WG)GV|oqzx4R{jhc=$^hNX5N|oM=GS2j+d)H|dGNTus#wfLxTsj$! z%WHHWJ=S;6fgJi#9UPCie9}d@dFZc%j5`Wr#=9 zb}|jYw{Hl`Gl}bAHYW zz=7Rcec10Pseq~4IXL=-(g2eqYxIfu(Z6m-KPFEaFTr~`R$M(=(@AG0jx}BxQDGnN zhPBa5-+jUfu87{Jp5z}(ewXm3SH)~>CR}K_#!2sd<1fmNc&T{H=NArH5o{4~iU(Kr z(2}$!n!$Jp|A7iqvc|ZTSsv4GS|1hBV>s)GtWO)6kIKWA0$Y^09EWtSHqf=MwZa}azc0d)tXlDn7+>7KpVB-i!Ft>ETB@;!t zt@M%CD4NQH1%kR}O>-F=%alQyfs-0{eL;8>@eFppL_}l+IMauS_PLYv1e3(S z($6W-nAkR*e&F!nkfwx9TV4NT!9LcslICg|gxZ#K{p*bS%A6+*DDTTe%57+J=k7Ik zWJq7iV%h`$t$jjr?h}6}viXhqi>|54AIEqxZV%f$?V=aeo{*U?uHxd4~S1v4w8Q@rK#%i;`&|n;piW5<>1X`$R6aZo^$H0R$4vff>NbsPpR`p z-tFc?w{nhR2Up956_WE36u}2wzM=Fp^eqo-Z-haSEydke4VcGKQb#fjzxL(POiK*h zS1kgOTZILa7`uJ)^Mc68U#-5YfA$eV0fMs%&l}p=6jcK7`Ob4v1q0t|3qf5GPDff* zR4$=kBQIVkRWj6At|%zl#wQR72GCTpzUNYC0)$6{&c7fh`}W^I&&FpDh^P`_zS!X_ z=G${kH5>7Nz>2D&@Kb1TBsmKPJ7Q0d&*KBGWjuyoQh&5${-6U~&d|Hz!YlV;dwTU> znvTek#8Ht;j2K({KYBKO=0vY;4od9X)e(7DJue^oU$8pQ5VRzoatght>sHlw4p|yz z{e1Auxh60FRZM5Nc#z>hWzx-sd(UalXwv;}UE%$mGFe<CTbIyT0=?q^RM>FIq`qX<>WhqhO&tKKYrdIaZoAOpVI4JXKpoG<5wia_Zq% zOnQ0*OKbNzX^+jAcH~DCKhE0HfmcnMbHi1bxTJg9Zq?5@crWsO05GKjPricd?Oz7j zUFd+&i4*)7VlOt0GHvvx`y|y2oM!s?R@>R}L>_}rG0lk~Y=9gU02O5EL`BE}JS#h3 z8n)mHaThyyDza{=GpZ++1M6M56>kVMy66_raf`@t^DjH+kJnpKcCKFhyp`l6a`59> zeNJ%7m&jwUr}@(?-+vBJKU>ukX(36?M!${VH5lbP(7q@#>jc;OW2=rLVq0F3q04=*dw&_rj;(fopU@@7KD1e8YMf|!FPqI|m9hU- zkE43(e5&#^MrLMdYuUFcOi5MX^E*Gewu9u3P3+(kr;bMBK^QaON2?d=n8BojpZ zYrC|wkjaxBE_6~=LHq>LIFW#%gbFag6N!vyk#mE|O{sroSF{FS0S;XMJMe)kgjd64 zjEa1FUu(|Ui_gZ^?W+3}l75z&;9W?)_t~uXJjUY1_~adF9@WlcH6=O6VyB|6S4B4A zukRTA;#-S5pH+4X`fhvoE%dG%`Hin=*O>L6FCuO&BG_HEqiXXeVy=eT8J#x_cLy6% z9O`f%+3h}|;Zvnk7?lu^2{h4tw1qIq?W)D!wy^`;$?NvSd&nx2s}} z7Ko))(hy8_K&|V->%0S=NdI!6>3fs$g(fE;KhorTSHZ5w+~cg2yu6Qv_SeJz(iXwe zC>$r&Eci2ws}iC!-n7o`g;zX64su|sRwYO)7Zc!Goh#6E;u3~%xXb7Z9OvOTMM%Q` z`&iD4Y2sY2EEdKAuge@!x}5Uj_Q;u@Fb1tQD1a`X`r>`&Q)6F zPFggnG_*Q@nR~sLziRxK{Z?f87Df7*ZbqaDZox{l8>%{zWJedU_PC4kHV7pq%cVP5 zJzTi4Y)V(YMIS=9!uAIY&ccwD$(Z zcDTS_gt0GZT5s3sx6rxYIQcg)@&bhEA;p^A!p@@EmT|oLzRN+!WA{HzJ8K=!c7;o z!v5WH;mslUT5{4>7!R9TD>cvd;-~W-!Yvl}c^(#Iw9XR)r_bsBd#fwvL*2_+V-pLL zwdp?dh2=l=sYNcEXMI2BG zujpK^I*p7)OxQLz9s>?F;gC}#G%Y*eNF<~meh8d7V&iOU8RM|MvcghH`P4mIcS4Yc z_1wr-9&9(K(qqLd(z0+`fsU!ackfmvRq8dB$8VfN>ED$+MrY{RfSMomDhTzyLaQ3j zN+w%>lq=9$e*Z;3tN*Hr=-QuF21L_GqaJZ|TZu&eS`A8r$N%b*xAMk=6SGRao5wji zE8V^!7OwfNVUlEkEhSl=Ia?*9J7i=%au<$og-c!bM-a7?=}!_?Ft5LB9gs7S1K`gP zM`)d5!ra6f3^q*0wV*M_=a3KoJSTh+bGscHta{!W6X&TKbA(iriMf8BeZ{N>5mD@( zmct6;Q$7LdGzKZ^?BbG%~*t;5IqD zx5`7IBC|S<9^l2%{MXb3U5NOEtp>C4IDsBc4%NB^z2QD<6UVg8b5R_4z_}V1x_H_! zxm7!tyYfKEOHe#%9o@U=a5}MOC-Y&}4wIZr%>9hUa$3x!i zblVtZTb-Aq+0*i_o)=C`uYwkL3FH98B7)otK|EhP{;{?!iBvyYXi|c<(-`DEQoFsa zIg=Jsb+Tg;%;qzer}?5tf^hEJI(2E6!7*q@S(I)6SYL5?*<#wMZ7}tYhHDJyUS%%C8_}GUvI$Ao9~`Atu_4+bn)6Th$0q*MFF56U@7T=YoGec zKMz|u1U4yzdz%OCd`g;Mqi#P2l9E(qR^jLxO461x85G%!Auq|bz*`KfyYgI!G`aM~ z&&>pKIXW{@oS*SffWib9FBpY|8Fn_h| zV$(K8go(NnyQD*t)U?_A%y;__Bg0(^a^q6qH&kU>w*Z)yS}9`CiS48dS3q!cJc+;>k?cKsfyTwNj{wJ{M$Qx z=l!eC>9x$mkNWT~IsL2lry`}A)P3((mR z0Q;GGZaK+c@$?9ju|NMp!I9k`rm3&JjJ3B|6|rj9T5RcA(m0EKtL^mU#=GaQ#wHEE z!>!X^FR*yX=vUp$qfL_Y{;M^35pxv)=@p^myMVhaF>aw4_I8>+2_L&eXMy(QJq9W_ z$iVK4Daz{A44+rNigHvRx8|*WANQ7>l6gMNf^o}N)Ptsk0H$WlJJuSpI#)&(WT=R1 zyrfC=|M2vcVNtc;*F&qMB00c_D5x|@w)vaxy%wEM6bg1)kyUF&*y?>d1lazvp>K9z@@;MK*od|# zX#77z3)vk#3ch>y+e4X~hTq7XA7#Th?GuWCbe=Tj(BZQ*QuYm6vR_mx$6h9FDGw!( z+iDEn;(2)N#X9!`8}v@yC;fPILG{w#D;aWr$@oIiOYg>SGD`9|JCcOptJR04ivIJT zj$Es1bQ``@f;GOqMveyWR}Zs0!rrLA&>vSweUGcgc!0};Urg4z96onDe4G*c-T7e|PU8>0u8VzWLn3#e%~okrazgUL5eE#j zU(wd$zD>kd{Qd>IPu((K7P7cD0sb3Z_^=x!^m;fv_+u4QFcVH_z}yvGm}Q7Ze!2th zUmx|XPgLIPFLR$3$xtu1C=b`NKct4a4JrIIzG1%jhc)*u`59JTNb8AFU$$8#x);sx zllwysVl+A=hJcC6>mRKoQq?m$3`Q9|6ZS{=vvpv&x6Ns)rRGzMFYhJiS^13r0K!H_66}<}K*Y>A~F<*vcrvDI=%XE*rKj&pfGH2#hPdZu|*mUifq;#`yMRn)4OMq#k`e0dI*q&A7W z`=sH;4hfF>jU|mdlV<+Y$2Qg@1bd=|e!dJkJOZ}w;d8SGbLVUAhppo^M0cscvZ)!US*pshKNr`8JQ!x^1fDyWO|)ClR{VJ zGnjM(=y&JqMDmAVq?P{;TOoJArE@@Sr4;**tlno*e%$e0WCY$5%GG@uDrM92kQ+LX z*T>cn%uvHL2ADfFvos^l-M(ltd7=}ySqCGMJ9(?# z-Txr(hKO+dighZ`LqRU&b~*)6HIo>ido!r;R_gp*>hC=|>|x{#GYL9_eWw0S?=dW( zu+!rrJ^XV1%{oM2+6H7n&Hv(t!?i+z`L-_LeFmrO2m7h9QD!~KX?VUV(JkMrf0{R5 z*?MGJD?J`7q`jBazw|MWScc+5nx2i(6PfnC@n{k8togSBAFlr40!nWv;cq_WMlYu9 z&Z7^$?FvZshS`5Sm1f;|`ux1eDEu&cD9f#IQ8akRCcU->2NZBZUI@EMdssWWAHC12 zCtT(0K-YiGfl=hbO{Dk|kTl;x^G_Y!>IZ$B%BPpC7}6%c5`Z)^CM?w(BtWRQWr5-y_xnSmB!vFOW%k_2fI-P{ZTwNlF0}BvTR%_`>v12;+nf1T`ELgSoDmcx;BKM4L=2(>XyCz(zCbz zR^lTKdvLSO#ZzHBo^4T!BX!Z7D_dsh!!b%I_A_Bhwe# zps#SvH1!*y2y&zQrTb$k$-5D&fRTV^n$`yb+?5;6R#n7!0H-@7W$aqdZtVuuQBEro}sbK0WF^>zJzX?Le}_ zitv^ii%C<31+eSohbio%jyQk&NSNsX2TcXgn%x>Qt}Q1<9#ufo{Jfa-Cmg~t78bh{ z9_ynl{?gZWT)aqwuCfzi3u~b&3snF`nP3+*!B9)E%|>WYl(q^336s%OZy;_i$oQdM z+LC+k`yQ1Z+$P9`r+Of}>K4$-91XHwwGr0uS@j z6R%JKU7t9rz|2<;15SLh(AZ4G#HdO(xBUR0OR~Q=T{ryK&SZ<%^4`V!D}qJC)Ao~Z zVktx0^yaNe5GOK>kBf6@$*14@>iIr)(*?b{!=1FZbnUu~6rAaQ&zcAIdJpuLwTKqRg3rGA1+&3z z9rAv9)cG>mbTxM!;s%d0VSk?24P#K&^&u$B|1Mr?$7)0HAX!k|^xbIO?-uq03B<;J zM(yw{TUYk6s85-|SsoZ)88#)qG%qWsKPO%L-rsn#w*-U2v*NHP_nJXHUMj(0^^fjF z0?+|+kv2gq0LqQ-x&NH6smzNL(QGwYa*=~p_nZhtWf%p!BpW5`e@aXlQ{q4JADq&> zuEJj0r=0SY?L6lVH!1tp#XaEA-8&m+2*q@swZuN$o<%^~R_@oO>PKPv)QYMkkuJ<9 zg5HW(Uj+%favb&d@T~kk?6M}54zo7%m3Jzi)JRd%(cVU_wWg}4tda1g>1M^SEgArBVO3cB9E`ZrUM4zFba;0Gd)4jl8KH4cgt8pwtF5uM?k`3#Ze^| zGGEs{JiDtp&tsd1^=|W*KkJXkJD+8No@p7w@wiwwcQqP%?dv7kEhDG5{=vxD$WvRSdf!VO+?vgqdv*gam9%hg}vUxN=ie%|{LC>#&8vo72BKkv; z)&wr(GoS|=t(HUjyrc=AFR>6f?X%iWR}zen4s_((Jl5Rjh@iRtC`g5vtJ65p=W5k^ zv?4yj{LWuSN;Y}(4dD0F(rbkTWKJ#I;9YDnX#$A};Cp zsg8|Ix_EAD*>Aw91=!9u$VsvHL%|fg^L$)k zcSom0G~vv2U@{3hsda&Q=^nGYK?)cK3d3u8PsI@U#^jmj|ArKAe+>7iam=x{pIqUP zn0-)+2<;H3t1A{Mg^FmWxucdDxZhBW$d)ja(c*4>B*6g)*S?C$K;JK@iTQtV?Aujc2Rr-6@oX zQQ%C}jEAiesWDQ|7iu;(h9Z@4Gq}kikLp6hF*rXB>KNhVMoh#ad;z6&QWk3qX29XP zdOKJ*QNQ4cy-N|fTb&>gfS9zyJp4z0aL|RQx#V9~mPpEXQ{a>EOyQ}#p!3K1@zG>2 zB`b;1YqN%u#mdKj+b0*Lq}xN<_y3NicVp2%J_Caai)aI)3(a~9ZXA6)MQv69bC8K| zAc@sz3v);H3)1`p_MPZ8B5G;Z-ZICjAt7>p$dnTIrzV#!H#PH>$LSssi{^7IbAOt) z(Xr(J1boKXDp7nsCn*;8N>mw+UXE`^a%4rEGE>N^!SgCiB5q`)p66KqIRp;sn- z--d#63?k{I2mqYfU(!Q{6l%urut@~z{7AbVaaX0yBa!~JLLx@r$aL_zW(n37 zWFfsJzu?ex5C2#?nqcYOGU+uJ@+1mPHuoX_t~UF9&|V^w$>rxz$ojHZS81WpvK@@c z6KR?Tn_(l)diVp`&NT~if8}`BN!Dn-A{3xgF8d2;xlSbXKq`-9_yE8N?ebb+fN zr9}g#wua|PO(`Bu^Ek=ccPlei3M8-F!R8d*uOG@i+U%N<2b@V7H!hFNg7npQ8b5w0 zku?$ zzdLwIZK3@{^~$7d7HQ0SlNEdvkM{+r)5^2UU~GJwA52%Px;dt|)x#z~Rw=vo>Lh|j zd@~o5VBxEx33u({cB2NWnfmeW`^U@h?;=AcS}njvoF`DdnvpR&m9w-N&5^Le@)dKxnyv^=X$$`M#jzF&bbb$Jg}&yyn}Za zL<#WBbc`FJmM?cfT^#Hs{gPqIyp1zhiDg_a{GU&v#Zb5^6D@Rcfy-xqgpflXO7(so}_ks-TapK`&q(u0-^|$Hosoxyb7Tt+O&p&&lsP}9}Ps;4Z6#sC;=@BX(97S(HWvQ;{5yD<)xFwPm+*;DY6 z?Twu;#>6+4>YnzJXLjEMV4S-~nMd-6+*FB-WbRQ{(3C=c@Ns-#JX5?rc1KKMrCldz zL@`3u2aY0-!I`FUJDPMAw?+rYQlU?SY46=%Lpk4DsVs>v1q|vP0E{oW@iu!Uo2iFo zZ?5`1ngzNy`3v(((Q zfK3Tj)Yih_1*4d3*%8DNrQSYDxeVm0DE^TCcyyF?$z1Igt{FZ?BXTo&IL5g;7#JyPR;82DR69)&4^+ zXZenHlwNc!y_PGMJasVV$MO@&Ir^;cvm*aYmcmTxe!FD6)xq0v#yvif@$HM9w+JLU z@^lq-ck_s4OaXqELHmw<+0;agQL={ zp>LOQb0`_@)38_#TX_ByBS?S5o*U*bJwvi|7;AHd|^ru^4U zGow(CLt-=Yn>iFl;iKTY8gqmr&CO8V*OzSt;{gg;C=< zeQD|jK~n^c`I=Wf)w}lcPM<-Wj<6@hyI>iJr z^N}+y5)+)=1p0n7nag~4e4(HJV9M@yW-A(SDJQ{mvpXUn;!|T&fUMd%MXAz;%aN8}o%5{4 z?G*f+efeDy*nlb^q(!zEu_U!oVMWvTQk^Ktm&~c3(tB4&Ls|Ulo9rlobXAnR?-ms? z$$6A&=0%y73J3K}9-iVH#)@0XQQ~IK^}O1Ea4heApWtrNXnLYXZis~6 zh)%dK$zS=YMN{GliN>gmO@T4~?@B4pMdp97XmH*RQ*%kHoYB0K$i@jp3i@V$`g=i2 zai3ZfVx6jn32a*zR$aJAwfyv;E|yeXsd^A-6~+u2eT@FE>o?SvvzeL6 z`#D0uVU8LOcG{t*Zz__&pyGA*a*t3ymOLil*As&@h4PNKcEMRC}%Mvsc4K z#Q0X~XZJM&;NyBonC+E?1QkBDXHIFhs~WPO*ivpD?X}mecyE?zCx2!) zv@vck9l8p+|KiAFO>NUa(0UV+7FUh{4qqe{MTZzZry7*}{$Iz38D!5WG*0lTiiESR zH`ZFRv)1{avLHBiyF+ABO&QOAYdRDf&YUS;)g7dr>OEDNBeBJjDLOU9Z@3i^lKu%9 z7X{5OoqI~&ifB~LFyj(#b=-FWsKpHIWye8+qjEIpoPTrY<}8#IaaL{$Q2P}{2~W$Y zidgqhYTK=s`kI$k+xB`U1j9@NuCiKo&L{UXTkM*zTC`bP@S6@NxZ6yRJo-j&)Ag$U zQjrefXOw){6dh(UlY_B>M&ila;C+}AI#wnDWKJ})&C1pg`To)Fu+&1lB!RZK z&U=Z+ptDK4i@2J>JMGNY_e)phJ_WO|tHXN~C>mPES+}&RRp6edWrU{JutAiN)Ly@Y z@YUfNY-kyxQ4PA|IxJ<^z#h;%gXO#c#}ZL(lLdCOB$iN}$ZLvz4~a)RmJnf9dQ*;wnocu51BmN$`LuB!geuO6 zejAAxUNksW5GYS^es$yz2P)R&N$h=XX3DNCoz7ZddF!Jt*+9Ksu+Kn3d&%-0)wRy} zyIlWgk))~iN-BuKoTEy4UV4Rq6gV(3vT5CNvdh9bu=b*4JAY21u*z1-?sDUHd~kAs z$^Y2(lyiqh<$>R5^Kq*9_XXP{Tf7$a9PXL!F?Yy@9oEvPtfGmlYx0usPPA`!2OuqH zR)fPt$^p&>BG>#=(sPOnxd-f&(+6zumi~k2k>dMk)no<7=RZj%U zbnv{|Jlo?&G~gYaY`Bx-uJ$G`J*sdPhz%+ARiY-F_*z~^TC-T}q@RqU@R4)c&shD? z@I4MXjA8vG){5opHKB2R+v_M{R1o57%W=Azd3a#G5ez>iNfP~D=3tAln;R3Su!c^g z+^A>lQRd$pKW*JFGF0?!fLz; zBT+$an5iOZHF_U#8GDV_{N5oZ_DP6g;?28^#{Zsqc5AfUMa6!!_3hJ#?=fqwOuUQF`1K8E}5Gg-r5cN*~d0^b@_zV4pNF zYN@XQ?7EFU4ahMwiHVK?2UcXTXTP$2V>}VzQ&EgKS*35ilAg!qvck7y&k1xP5gOnc zdj>z?CuA1oeyTv%SguaF>d@!gF28HHf-}tycZ?{Eq!da)-a23$yWSjhT(DH=Q8t%V z%>eWCYCg>LDIi)o>2saWyn~fRA=Img&A#A>tr{WG9%F7drp-x2fVA_)>c=qqYOXH3 zbc_1P`KaXx@;WW=n!hD0F^o`Br#-mKWXR27j`Rg(hcKWKbr=m^ArFMs8Sz~P^t~2h zQ6&j@dr4etD!!uB9Q~!bDyrcVFlNbQD)hDE`rJAm#XIbJ!JLdV<3nm(<9E>T^NEWR z@4zpXKXV(a@VUdcHI`e3#~82DSWq*UCg;y9qiE_k)9{GS4bvFS_f~jK_L~WRC5h1z za=EpE+gH=NyI0F!9qVt<_4e{?W|RXXU$6g!{Y)kW895G`x-u?I(XBaCK?e3^Htg<= z>eHp%dY~sBHr|6~$C(aIPWuy+4})LIz)4KHmepSj57~{PFGc;M8|YxV(6o7w|XBunuwq0I<`WHvxye%ZC@TDN%&IcXnr6O>0ey1Fk zM9*_=dKd;+T8}n#)H{8N(3;l<`yvW{j4^OSQi$y)2QD^r)}Qvy`fy$M^(NFXO$~`K zIllQ-@?0z~{bOP2Q1o)Kby9Sig-R1?k?!(`vsTA{_Nv)URo-Ufa)%xr^%ONyg&)Y9 zPs&e~m}qUjCR=#pj~Ftl!mOjV5=BvKG_Q!ys5HDxOWP1L!siFXv-Za~LlV~xJ{JMvw^m|a*-N5=3sp;`2mU>i~KY|=&=Lb+@17O~jW-M|t zX*DTB;S-p_^{7pj`LAE*_c4tmPWQ_~N>(FFg7Y3Y1sdGSlYXl9O1v)|zV z`DF;kX_yA9`Z>-lF0||iZ7Dj9jZ5yR@A8vj!l_D0-IBx>^_x@WhOQ zJ(eZf*2)hH1CsN{>-wcv-WYKO7)^R?b`E(q>JY< zvF_9LNA5RweC^6Rg~XR_{ZM*Oug=LJ$LkLGSqWq&_I}2PZb6U?7sY)FYP18J{Gxw> zh6VzYe^49gj-5Nb=<43CqGP&1;=wJ)jySE8Y9xwQ-_#<6m`_6cFfq!b1%O9)Ptyu< zm~FV7jrlm;;x+uu^Sv)AfhMMjv68r4sWF5C{64C{WN9A7^k*Dy{Qu~fvl0?X=iJug zOQOvW8aQ$&SJ0C!$5v8B*is3P4@B{%CQIT1`U(NMm<0Hd8!i4(Ct`;Az zkhd>ZUV$10i5rECP0;XxYBlrR%GYF8Am1t+YHm=J|Hn<#%W1HbO(Pwt?RN7Rg`#BU zCuw==cav!p$QtB1V@~wt19R-fe>$liX^SPK#F=N`Fcb@-W{W3dyp>i3o}+XM@+0^! zE5?+8KjKu2y0zDp3;YB71p`BMTUY-jb%(yZqbE1mD!qfATzDZoI}tZFH~TP&y%p6P zsS_+K+8Y~Y{QGykN%peK_<0O$hg3fv@V_r(-Edh=bKaWBMQ&m&Qzr_TX4`VP6&# z111iKVYnBaFVEl&^QsRBtK%Yu^L7<)tQggpt|ZZsNEJFPUO_NqjB9x)tO%?VB9d+s*`h87-toAHe8 zMZtx+kE2IX${S)f@iL>z6{bP|y?IO^QjSYY%R`T=Tqq!7*5N^IgHZ!XPv<*U<-b!Pn&T z+W`xV9#;*H6?QT59prxGWwSW#GWXJ;>*DHG9AK}?oJC-7a}QI%)e2_l{n8E9LZ_RS z;gMzjb|WMHYPIwNb+wfA9I-p3G8a+cF3Rp0>-If0xfZfJe2H=7badLMK~UefCEr!7*PevVyhtcZv#& zlG#J)=*$Vz04clB<$qBpOl|2JzJm+Pb#Wi8I9#+{G?@T zT0wS+3e(?jOpo$Zz$tu|U)67G6h43dTPIP-qGA-o7qJX(ONxvSivq5YEaDxxd^6zAvT1r%TVA|mV&g@C9%pQ?z z%PXhOQNs62t*)cbXMDEaeZLW}2hKxNgB^$;$7zR4M1uH=pXa|nsCyg*`Ku0030eR4 z86~8c@hsCl_ghtV#w_XAD19PHUp8*qJx?d;MAJkp9B^oqk{(IHY1TwV7uw zN*m^*V%GH#nt#hHcD^Doadh9H{LmpJCBEc8D{)_*W-yw6Hd%4AGPGtA9?<5pSVrD} zwLt^SS%xvb!5FVC%-+7bhdb*Ndb*m7ciOm#)2vP#Emnd^tB-jWqE6Eg1MBrmtxRAWDIc z0NS7K@dZ4I;86_RK`ExsVDma4UvPqL~cSZ1p$WkbICw*|*= z+d6x!Q{*KoWqw`wKaR7t4^na!K3`49)JC0*;v~ zE1v$|S0PqqKHFxB_86;!$3c**PWaiO7#J)DhG=LPvR}-`fGAe8+ZVq}F&?aRabU|d zZ_5d}w}s?`k%K$T*}u2c-*)-r{)SHWt^8$mD}r%;Ryybx(GRQ{)qOPD*1M?rLI2!# z0hjKd$GFUd5nDz|iT3gm_0E_qlk$+9_db5h%h#lmlQM4Z0@OUu%!HXrh{SIvxV*3u zCu0+h+p%Hj)PepK#`=Zw!p>coAL`A+j#A+8gUi=|S;OypE4BVzD_44sSgm?r%dHZ? zTb?u$HVn$ z`#DSJvr9)tMl$JCuVPYOJzKO&(%v6U>2B0c<)edmy&w9tvcLH#T=J~uX$5>?y+ihw zO=&%lKR8q%hZrH|mVaST+n1g}l|j=?9iMnl+ABz|)T21)9)mc2J9}>yrlsq7-l|#n zJwp7XynY_KS9-F|Uxjis>#J`-%j7lXIZ(gWa!XWQt$X(O-qSh0_w~&Vr_YV;0lcO> z^B|ix4Layd5y(BB9b|DT6al_YUO+yy9J|q1S7bx>ntA?=Kbq6>Ue_sg%r6FA(f5b1 zVBPvTOSOCG=XoSu^W+cd=PTmu{m1}3IJWZxc}28ai4{Vl!2{u|DZ=-}=bLtI zDbV$Pex}2ISy%=pbV=BG07e>!7pRUNo_SuceM9fjN!`OgYDVn1pXrDZ78b5wBO(wM z_Pm;Uf4AECUc%+5?TvLEVhu6La+hGPG<>SRG(}NIZMv9@?=C4; z=BNuV_dKK=_fkTk??uUf9ZO!YDGJ+EUPF~?v<@DQ)~yU(kflRY$9{Ip*iwtC1PkvubG z^duo7x=0>VxcRjUw@N+f^f(CJ6*9;$tgXQt9Kmm=9VHXDAR=#k<`ohc|b4CD5gVndSU~L)nr;hWY6IHE1@{ zeRn5l+P~_r^if;=rr9QOA-1+60%ffHDTD|5Z6PP!Y!~oL472SFi31NGRbEM%r9BYY zzYJKhMp@GuzA0kvvin6KG@sexlqPjgBZTWM12pq(E#MyL*x!kLZ@12_>NB%P#CI}P z<3JDm+xJU&fG*4}F}_g3Za^Koa|Xhmr*0ib4SI`wpV8#l7!8+I1)I?`{i}~5w0$KT zFC3(HN+3c65-=>$px2@9Q`ccH=OgL?Zl00H!L=CNBJ-mf_lU(es#%lV>gHu`OY6~@ z5&=7|N13x%9ROMT26zyTVIxW44ZhRqYH3zwAub{)2=uzAQ*$mZ5SQgLQFHhcpvRy_voZ%37K z#tT_@tdcEL0&9j6=_9@Ghz*p7KI*+q<~GP=^4!oXY&mL!NZrLk$Ib!bPxh{DQ+qt> zf|BkV$%+Wf--U6a+lwI?-C^9(cuW>G7}DwGiY^&SNK9PQTUm zv%eS;6%Ld#qJL}1r#7oIm`31?eh{}!j6Gw~jaH4CnTDMBP_5iD)rciH2DJGB+eh7O zKg_CZNUvJn*zFyGZ~G{i7+kOC6k3E zQ2Q9qVL0(h0+Gtv$7es99lj+(boSQlIIZ=y`N&e{!)H3bz$CeOFo~lX7 zQm#Wk`(9A;ij`D-^j*9^37BXlsn{?oJ!W?o+-H=!-8npiExiiv z6kO#e{U)`Lr;DeW>i$hb`|ceqD)i8>TVDM6aD456JbGztxz->rh21YMjFh)lzFc_X zo%R89{_HUTfY;GsKHOnuKt%lA82wVtZekDke$$he?Cy`zcD(mGGWdQMC_GR?$~zYC zFqJ-f9_}cFhjk<&6n5)sYT{0rC9_=wRW5<(I=2I^=ZcYe8KkIZe!4AR8fee9r|W}T zBFJTGLgB@`Xq=TmL0bdote3iDqhzEZu!J~uHYK~u9$s=}Cu{wfbj=gsCH-~Oi{?E= z*q3C_Q9h0DFWp`@kZHL+YY?gWnx^FEmK+zT4f@=LWB(}>B>n|D*t14gTv9)}5DG(o zI=!^$nSS&JVSA(>~ zEo|jeMW^0OlU46!)hEOk-dUmrLQNAb^)Z$VeQLC^-%rNI!{FKJ!qZPxqtF<;tUESP^upRpVPOdAznG=jtS~IG^zUSH2OeLxJv}&ng3ME>snUX9&dGt_`BLl3MCj% z-lw{Va$l)eMCtYCmTD+(=Js3Kx?^j^twfUnIU&wnjudlTey5F?B5O+ik2)U#;%Dsq0eidrl

i#o*6SXlxwE$K+x_J_JsIO&E<*{al6zm7 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 @ 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

+6O^39(uG?5^`3sLrM3} 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∋`6nLfpdb}F66fP>V!Uj9S+cO_Dx*NW8;O?eb`;xLIR?ktSWRLA;@Jx!A z0fP{4wK?bt*X|@j);OKE2QJo~`>nFXg+Xv!b{@pXIJ9odd+oD0LT09Q*tXk zY@^gWO(uKom*RD+{=#ZN4Ncw43JSG^P~C#N^**x{qsZUzz@!o<@Z=!`CpX@-Syig~ zynG^}5Xv;s!?-_K6~MWDi~Hy6 zdUc!W&z#*SX!grc8zL7GOha%Y5(kM<|09)jLc z=^rkY;u2?Ey47^(v&|`xv?By*Rq%`DFkP946!gnd7rn5q)D(ugfjvb3A6a8POpeNl8kET4Wb;}ZxmGO#R$)4If0MxVjlp7uVQLviJWrE-na zBkY@?&}xk`y~3uN362k1E8oFFm>kR1lkjg<3xDK>w1lYHL+xTcMPIm=fw1mF86DZC zotBQ>V=ETP#SKF(?fKP#S1qavMdk}^Zn6V;CP$n{a%bLjlIxiX zzBp`)r5=6vx2v8-c9$e*tbya`TEcGetfiux3|Idgg`zBJgj)>LL(KC)8#j8{ez4Ze zAxhFUANlrN=;fomC(3@!F= z4BIM_!PH>Oc40^L^^J4o<>g4<$sA9}8b`-O%bdmsgLIfFeDXx@Y<6~TXYw?r6+gYo zu#2Dy+{oS*n?ARV0W-wmg@ePHHeZ`ygE%pQE8izLo?%mQy!~ZpjTcHga1r=Zb2g)s zrUV_WfB%Xf_l653%7^sjCUy@npE8n%=|0gR=O%JTb=@k_$5Jr4)(RGZYdRDE$w^MZKCyLFN9;HUtk70-{MTZs3R-{j@}LBoy%(Y znkfHPYI5Ul(3}*sxo}2 zX&$r#B_fQYi{bRC(7}AjDAQ3W1U_(~j2e+{O)M^X3%SP02=>A@ljfAg1zD$ zkVd!bpIm<}j*P-;*SF8SKl@Su(o7P6_1`8jP`f7E!ScC-xCy9=E&Y4aDZ3O~z)>qr zGb|m{f|8HWs8y7aIW|&-^I+uf(v*7c#1SeGq#y;)9il34;WzM$cNm*p{ufC59jFp= zWwCnb5)4T~)fLN^tr-(9=>Uc&?~nes*%@I6q|F|5b-fKy!Ofs2J72O}<39^#*9%i` ze9hCrbTGB1q*K{>;*ymB~|L^04#_REXxyc2q&ZZxob- z6f5hF1iBYE9&-f@BH&VZU zs|~@;jMjda=KTc#3d9njI+hp<{~qDYDB7mPLX%wnvW9>qCRYsi0ubXK&bwB7@7L>oBkL49?9;B>%@!pD@ zM{Q)*k`Oeg4rJyGgGt#}PrRf0$+UF#)A%>j>Gd3h9&5rkNqK~ang0+>aW}@w_9G(S zKFGeg-WS47jg}U5T^pBpNp`7<|`yl@RO znW*!=OBGjlmcU3IB1*;QP#&_zq99d92yz_1yatsRgOw1J z!mJV)YVJCpeZr+aIrwjAz%oaj_5T7>*NI>zA; zx#KQkP=nUAmQ!{{U3QA6BUvdrFW;MNP@8>&@m3fSw zwb#9$LY|-HN;%G%rT+=gZ~S8+NJA6uJ7ZM%ml9?9V4hj5b-yh=iRYq=xKq<~(IIpt z$Jk=IQ~^*|i4&mo|DIA7&z^l3mca#hH(9tPS2-F@rnHo46G4@tmlnns(uoG(TI`z0 z!Xh{KzPJrWimz#s(d(Z@1#dQ=p_tFXsl8d&4BJ0Wx31sY3;j4eu7mx|8_qxlZW9#t z{hZj+xdG>t{w}(>O!bLAp1WU5iT9TfwJt^Y52}z;+NWXr5IwX^tVFDZ+6$SJjrGb`TFL!8G4un0+Q$epqtgz8d;dY((tCX(P}jn0~n@ zBjs98B(1=?==rcf*vM@K>K*}=Vx?5^m3L)>|NeRRvBIR3y7wHEarfvwn#YTJZ?yVz z(E-bcomVN(PnpBY_u5*vDO#HlYlW>=gz6LQuEGYM?`4M6fx2f9NuH9oS~|`chKE)D z3igcbKekm|dI9woCq|KS+hNmgqBP((IholV=~$WxMiMbu@MzDUt_ zR@-N(X4|+$L3~Tu%XP@d#iuqE*}2$V_H7O9Be0{Q%ON}3hZ$ghH02YFpH<#YpFN!S za^hiui=dzfnzQvU|6hFPI-kUAC1p47h9yG~s0X;F?YgMg{hrLy8?fh89H8!QB=IQd z5CqOygW9qqr(}A>C>A%yJHBc67Bntq7%6)@<1~Uf=0qz(IBS2hk%tYcI+f zA$Vn6Pvwf+Y|Lon!?ov-M6GD&%LjIQsX}a;?^ealr;{Sz3}lGnZyAkXoZ4QM?sr%^ zDQjXFCt#6qo1Rpk0kS`~AR%|sw@`1lyv>vU*H&pC%_uuOQTTK${)ZKnWqVM5UBpb0G5oO(1ijmBrvP zb8+dz4MAux*yDkeZO4m{1ODpZ5Bzd}b$ASH7RI#PP4_)mE>g)w#?9og92WU$sVt4R zDWr<%teGUY;*7D2BNlB2x0f-N3|RkxE$%mH{Wro#XGhA{#iY`6Q5-a|vb?Pa#2hh) zgodhIg)X!7Z-Scp+0bVmpjw*=4MSLAs0sZeYSXR>_zxoJd&D@?MM8;gBhR4*s)2iH zSEk8p#Im@`5rbg#<^pxpa~~?NRJ~3JrYWbf3-<)9*bMiFrm@>=MigdPlL$^E%>n9c zj7?u(O+Tr6+`jF%$wE4jc5N`Gt8lztL5Nsz;7f%ueRNGhT?+5Xa;yx zA-|XwytXvombP%I9Y^@>64GBob1GR@BfoA7Z@A() z!~NJGN9wFFKMKcylkcQH$~LnVMaBPqGmD8eUdmF7o$By7SnJdqF(;(w1=dK0#*EiN zv^lR$X-6PGGJ|Wlq%PR6|=j0`d_WI9@KLV=iR~! zu-^akng@ORassH?^8eKWFe!}=HWe=mf8d{&rz3_YF>%KSB zGu-0|G%|GJaB(2 zBx`vxWBEOm?puH%K8@J6K6>|P<=U+MwwKwwPov7JwZA4}-d*W`ZJMhgJjfnK=2_F9 zkMLRG_>g8XS2qNOg(K^Z9QUV9lL=K8cNo|OEmveqgdmnT8z@SY1NGgDolDVJZ%E+8mZiA9Abi-X z4!3?~cS2mT&YoYozNJd$_Kc2MXsB0}EGVHUJA<)&rH)WV*NV?O?vmsHCg_%>eL(PV zl?mp)+~*lxc-2B`Yap_`BUKixBa?ZEvZ`fGcnVdV9;OCg=Q2hJIlKdbkFC}G84cU? zzDNQvEjMi-XVKYzsdYCa76-Ppsd0RYr&n@prQ6aR?aMf82t zB+m-ZGn=fBo*LZ?IPqwDT0v^}fmws@ukSA_#s2K_o4m;&vvJ1aWr@@FSGd{StUTLl z;-g`_{sMW;_|C0la{lH7twf!%M;eq5s{@NrKKIh+{r)Vo zMQ%)&5-i+y%yv7cnp+|^^>510L0)qT>NKM8kOZfHyschRT4!TGiIAs}BBJH$<-6ToZ>r;L5?C)7&T_Fr>UDow zcklG)E)=(Rb!)&0KJRn#4{5&->{{yn6*ZOQUyH+tirC~?#!hA3-&Hv~6*(%+(Dh z)ok15J#cE?_-r1ByXV_v4hb;VI@cd6(-6=_9-ZB}<0#v8M;|-mA;>0+2`M{|FR{@< z`I<1*Prq8z@A2uR@lBIOt%?dc&rl&WC92O4=#OA^ zMQ)SMRD)k)kXlm7*DCrV6a*vjE1^@!iT(DNeEjf!p_FT#{b0@m3o^$NBc}!bqkUFn z(qPbN$4kQ3;I+&JDDL%VBTz_M6g-(t}lrIw@hw0ZB{+7{9*q$*{_3>}>jb^4B(CMGBiq)tx@CbaDPmDeMX~9kP=w}yU{tR1Xt+q+>L4s665mORb z13Vh;ZA_W*DbV`hYh2^Yx%cupVgbkB)}wXZBEQ?axGw>tQ7VaUx9Wv&M=rT$LVt?~ zlE3%~5aBChkGCs$eOH~^nlHgdKBRHrxqOp9+wV#Scr{M9~m^IJ9|9LMqQ!^ z5^gN8%o4&|r+Faipvj@o_|uGihr!JzD1-?MY%q&j`Czdq0omYt3?K0aUc|Bi%esU{ zb?=_%8l!`C_aE6SVy}~Nx*IOk59%(2^GQsMKJXR_upC&O{hBs17K{W-FLW+_$7U=Q z#{Eg!9iosgXQvcLYbif|ce;E;^cht(xoX$FFzlybn$tCg$=!W^XxXYGHU0E8kU_5h zy$WGN<&6EU?Wi|cDYC=&ar(B>?gx-5n`YHE4YYwCQ}M+*D?1|f!h^@3L?(EVibn0x zxtqjm4Ob-{^9zLkbI&?53tiR+k}r-8yQxn#u%jw;RLmCh=*C!Gj=?DVbgm8iizisq zmKvLMm1B}xLhI*ql1EN}zt5oQRop6cU$D(qDg#rECTQm$rZTyBr}o}+Pe*p1Uw%h9 zTfzqZO7J}N;f}Bx@+fYM1J4Y{jC8v{*2GW)^6ZjeGK#rY{)q|v>_+u^>=S;dd5USIVI;W)0JJFs6{yU%J^>)Ae&bUf#jBOwt4WVg%X`)Q4yk6D1)X}8JX!y- zGSvaJhH~me>caJOTg~@xPGie)IFReLPxop(dx4W@57k_;06*9o-Mz;MoZ7D12(9jU z{F|1Czk84xaevCwH~C5;fL~g>$h1$mB~I}#iv8MV8&vvVkT;Oub*!9_*#2}QqT*+1 z$?j*EL%!EY$~~c$#9!s^J-q%Yv${CFT9E3mwz_NlpK$h#Bvf(!KgO zTFKf~CuImm#ON6Ltks$0f0df+7|Ehme`;Oi@GXK#$^VC__Y7<5d!j}`6jYih2uL6z z3Ia-%UKIfq1QDh87LiV*g+N4<-g}RVib@CRCG_4=Lg)lYD1iW>1yXK)|M$K3emr^h zlMj3LIeTW!nzagi`STEN*gS~UF81iVX>ptQ*t^VNYe~j8Yyq*p*OfCm-oOkM4#pfil=jQJZwez&eXH2R4lgfG@-n@ zoaUA+JMQxeT%UuehUk+c${FIK|FTnZfSqhO1*sm-BR*+fg_8_#y;>}_%aY0@09o8O zgc0%GgH$(1=h5RAn`9qCi25#}kIa2&e9`fC^h#kc&TX^V_O-9jK_F1@4x6v0oAW@~ zZI(q!MR_VhTJYXX*hT;7o>;p|MTrV}Aq`bY$7g_{0nK}YdT`>oOW;2TNh>}POv`Dq zoGcp&kBhx}!=aB}n@LRy=wNi=*Ic}qUJ^cWSDhph|7Mjjr?a%{fpSU{*61&^6$KyM zILw*|?H<2P4iKD6W=`&8+$spM$Wj5_Wz3d6kB--Z|6=+Y{$lXe7=mX4_pz{i7*lb{ zdQ8MY)s5`Iq2}Of%-&zhlYU+)?7P91-Rv{l^BJ8P^3QGD*s@6ssliRs4LV$QY5WNK_m^)NNACx?fm=tBC~1340L zzF8Lmu%r2M8w$m()rVNh-oo(v%jn?gz$nxIF z-Uo}je($p#cu5|s{&tA4?7O`iI{J((t#{^9XcHA@Rpq+CYA9cgdRzU8<>7+a?08ViHChp;s|E6-t<=SF_LBOrE;`CDP67RwM4&@73MgR z|4F=w&-&F7srXfh+UfNBh6P!dso4aY^2j}{@^LGK@e#EJPu)1VPhG5p?glK%rX5cR z2lfc$nO|X_`qCzYbrcn*>Cm#HDh@^Krp~oN?IH^j*X0v~j-zsxwPtVK^6SvNqF(w{ zis8d8i`I8(Ki>;RpZCp~dD$;Ph`to^qR&k6GH=(G_;a9;&F6`+=Am}&LVO!XEqa2j z!+oh2*t%pH)k+^3!u5*Fc%nzuNkWt5&1HHAI&P?&@di2QC(B8J|^Hav- zTVjiSVQ$Y2=jFe(Uve`p0^45!hCF@t0x;GaDXnyJC7t>a%x3QEB>C@s4L{QLLSWYd zkN#5Ys2hvzV+H*#FPq5As!lzO<~)BIh@T2u_c{`4zbJ_OTS`t48oDFPMh^3^6U!Lm zV=jA(URHIuQ@;HbFTW}y%p!)2&}|)Qn4JN-U4||(w&30us!DkRo}aeLchP$ppmLU5 z*ca+ZWK-DW{)*Qb_m8w_4$&hB+fBnRlr&c-FHiAYz?ph2<+JVrsK%y9$T4@6f;j=T zY)|8f)hsPKU^3Lef9OSL);nw9XnhXsYdMU|0ciED=I=4(wwqd7}B?Ehc!@Jym1%K1ySU_#yYFm zaP3+~(00Y*5naFA<<@+}oa0$cpAH%fkapQ%P3yB5`1zo9KYRCmHB5TmJ>bxi{__QZ>yx6B zD7{yuuN53#2wwC>-gCUHwKVYPFqw z6Cgd)(FeS`^!mXhFIpz@B$A|(>AUGy2iaXjM=tnG6}UlO>s;WlT;CQ#+T~vp3_<<4 zR>qG5=*2iPl0Gg!_`WfyUP#-0gAOAB$Fpta9yd$7SK+^@0SFa57XUcKa}xGfEMYuf*-{+~neJ6Tl)$LekJ3y9e9uX-)^ z!c~_*&mNZp2-N?-A65!pg8y3qE>tYhNI!d(7nFEtsY;uzRx~M95u)`?>c2Ji_Iv+_ zXbOY|l2P)DZHDoC%_qh|e4bn1JU3G#MfUO(-~_Z`g8zjA&g07(%F5B$W?~*U>_Ov7 zOe$L4(BKY`=C5buByt&&OjPpQr9L< zOG!10i9w*kgF#1O0^LuFGx0eQwI!$U zW0tlwtGqv2`?I4_qMm0z)s>!34$Qn^k4SJdImqN(kCJV9+x5!1PuXGUTdJJ6(H)}^ zb2(&w7MeyraSrUiF(Q{*_F|unr;DR>Gi`F?V$r)-{B*pnXzcCbgnyznyyd;?*i&Xi!^4Ppl)&$S3T(38-9dHHyX0fwz9JKFdcLZ*%|~@KWs0)E+`p? zv(i1Ph}CI6%HFn)e7rg?pU|}RmPKPc&YmRb2spBD0k_jyCH=zCi(m%17N5AFiK4wK z=!~oenAoF1;U5G-c48m8?|E$0ZN8IDFhIK=s`U2;?TI8Od`?9RJeLRk-vB5GsaOWS zoC6uZ2);TRL{O5Xd{hMWJ@pyY@o0t7kHz?0i1=Td^P&$rkIMXBFJ@a`LB#5n1jI;j zlA3R_LtNbbvU$ywym*pZZf-*zF|``dz}M9rk^_r%v4LYdR}*WThz|rFNdK%fbF}qR zTS77f?6OHBKeMr^M>*aNZq__$pNB@L4#u#D@}6gOzdw_u6`7GBmO#-o1YfvSzmokk zHOBo&=F~>*yDr~5M4vQRi(7R>sjV#D9L4M?aq3N?Q+)E>E*)&pbfAS0^gj62IeztO z&pqsh#$Qp1XUL+d!N*~a_3_DKYjifNZ(uDFUB9lszO^ry!BgVG1$4KPo#}nJUQ+tW z7=I$-^IQ+F`7AYj!q8Wr-8aXnBzyah8KxLAQ@$nGYIip&r=1jLGI@7T0?I-_)k=|L7s=JgfGMZk1vq7sy1Ze zD7A4-gZ!Z!JH5+%+}Ep~nDoy^@hj|6?0*7nw2}ixqZ+)mW~E7AO1P?_g?*MK=?eZT z6RF&>Cqd;z)3GxZpgKMqBe%T7pBpu^1qE7^nNbX$sf zFxUy+KXm@t?1o-kpX7G9#D{nBVqV%t3En=%P1NIwW+R2*9h6|BgV7~ z*}czCI1&v-H6QecC8WcWqd4S4Dn0r z(4d8Cq};Q{gL&krZ=a$8eXr6`2wM8o6CcR?Y<1qRR+JZ7FaM&}L~g5!2lPs6aN3+b zQOi&i!#8J*J$X`ogH7h{4JLOz$p+{5khztP=>|3*!{!Q&Hv`eqz`I&Yd0fE{AMKdk z7tEqrhX}O|e1S-3^#i5D~T;kU1K71PVAzb->j?U)}~^dx|4s<;cR% z0-W_}`(HNnQtC=`+RpZ+mtmn`=!cI`z0NrTP@u@u)dPOfDc~)6ac|bkDs+jPa>O4@ z3|N3xmM~$bnW0I&1IwRe`20>i1?wq~%5_Mw zS;=6i9OLYhG0nUpZBj(C;_M$yO@k!qucdxUJ;)p?eQetCmQEPtZzDw5!WT5 zM>$#Cs$ZiG8eANtP348b<&B!T?L{AS>lH+XwJ!5kq8#OLmmK~$^+_wn7Csg&Y(C+o z-@2rvZa#ZmYg#Mg%eTz-%mZJB&KnNzc^mF)@09x4#JuLy;z8AIbmUZd!Qacr3 z+C37_^AHCuxBm-L%n0c|R}v(3y>2^CFxWnx$$zZQ%!6Iy-wh4-FO_Q2OR3FR{fMN( z#8kqxxc8&~ zAUVY^PwLKHRcK|VK|G~Vj@W7O3e%-OmsKI;A+6xW1=%fI`0i>aRs$&|aQDfqci7w|bI zLtTwcn)Llvg0w4V|0VsN9Fxdit8%S|JG}?#G(iigTLodIK{?DJb&lsmaaR(M%_gsX zW_3cNSk3i-2@_XnUWf^gz747U8RNy;_|7Plgxq!ogw?h5&={>*i3tW6^9UXhsC( zjz7MP0+LEi(&+-n zNfY`#oHdzuAi#S5UqRi1z^yH#m&@zZ#p-b?fhJ=2m=)4KENkSofe z*4}V7DmgT9J#u>OKCUkR9V1CVX8qo<3R1JW{?|K3@rMs)`EaAo}#^1WIo z{aLE)?NuIvF%s-n>46F>TuopP9d}tsn;IvYHcu(;WXVeGnau}-({NcHseEoUjp=Ij zwMAzULu70P~Mh?yjH&rH4f^X|jY z5cUD=vRN$8u223c&jx8VUisv+9*1r*Q&xxWlWEn9>N|TH7b+ZAc3A8GcBU>A6Yo)x&GY_UNfzpT89fj{&tZ5hZ;_EnA_E8URA6z&tSzvFYfksvLHQ@MfoqwW1lC& zMf!K>#$<4Uh>F4pzh`YP;18H>u7UFJW+a66w+irZ|M;yEF(TT%j8r~u>xv|{14ESQ z%t|5a>vglwT+YyjlS8t*afK*9n?RKxBAzj>ef-L^;ML#QkIfyL=%KmE8B&LIL0ddX%T|Iebzr}-uJx;HpSKMa9 zkA}BwydQB596!`3YT^%#6|bOuv+hf*OE)SS8Bi|K<*h4#9rBSo%_oix^KXg6J$^P# z1hki5LGOvDd>LUTpBr{>!SX%=RX^gUuu`sev={h-q&rAEF+|J!iRXK3%-Iw#0fDxy z@$2t02*fPB@dy)nY&Y)eEw$g1@`wFdiBdt|N()?vKxK~2eNN3G?pk$Lw&NDhGd!t1 z2(FOwzfE2U^t=wM_W55Va)+_a{tA4^pzV=Z4KD%96Pk!>?QdVK(|@_}tF!vBeA0!s z`^>-Q^V)-&3{SFQ1-3dYTH8mY$jHy7hdi5p?fpXRca_Ot~XN=wRmtCiHa7jyZ?()=-PN-*#O*I?A5rW}z8|A4EbPmKPl-Ps#t>{z)y>cU zQC~rg&r5(TwAoBkjX*`B5;&MpQDK2X1@{qfWEVmZM;t0sst`2v81k3wES8sP+p-yU zZ#4u>A$(vz&<|4O?RI6x54F^wMoS8BrDD5?9~rGxZg=-Jt5t4w>faD|HJR_ z@v)?Geu^VlJJIZpkgiG=@D@@m_R&LS?t+c8rDjgUDHX+^hP4lvd*7DDZ@u1^+GqA^ zwPU;#Xg33yEx&9x6I*a;yvgA9`OJ5$eq?9yH^hjZ|ViIjj}+<6qAG!T^?+Gp&$~Xzjbg(F@p*E4dyQ z)2R=U4q^>~4cbfK&XM4Ot=O-lM^+c7zt&8H7DnK9{JCCsV!A%*UK5?cQ@3^VCL(C^ z{>?U)T~i)B#b{w+_ofB^sdi)J?1JJZKVfy2=fHh@#R#Wy+Kl)^;Yy0HA`dcbLUFPeJ+=KAcxM79Uo@(Ozg#)<|UEQo#g&YDRvsm(s9wcF`)UoAt4+ zqIfU@OQlvHmp!a=HVs-3ZmhZT=DBURASjKaxCr$>&%0-uno74-&b;Q; zh8+!&Sr}Jx+l>5*!bp#jEls8sMq((!Q&H5-0~lq(R%M>1Qv0mw?Y~T0>`0OO@n>}c zp^~TiZEw$4myo-Q8UI)d?o_SbSUi1G$_r*W{XH$%Ijphimx|7mv>zwjdC{={J%n!^ zGEWS@kV>B{-5+NqT%Ql;$$cVD>`cm%x?1e`M%1q7QHkdFwz{i(AAekZDqUO@!LkFb zlGGFz_5H*>kzDky($i`W?~tk2#QFuyt&4G@tbYDjGF$C?@u#C(M)6h>2$?6Y&RDCbLaTV^j03>((?JPEVnZ`vyVP|^$JZIW`1{2&$Y_`Jkk*W~)TyJMN)LXt?N zN6z4)YHfD=+`dBj%8I2A$m3)hHrx*xPIx|+F5(c~EBD&rH~1 zE^o-wwRh^Ri-2~z#ILB7y_IZs-~DSL{ni6Eu3Quo4fJ<}Fh*+a3BHY!Bl?91$txbL zf|^`P;p${sMYOnqAT4VMP~n-jZow%P~=rlN1a4oL0dZrozMV@ke% z_rlhUlWeQs@dMi}ynku1oz|9axqB|zD=!{gb~f~zE4bW^Fmy6yjg2g3KrYhN8=i3~ z3>F>p(T#wEQmY~P4ZncvtdLGsWHY|8RS;i8p^52lkPs_K80G*|M@=kBR3j!g2{bTW zP^3Ichew9+ope?&bP6it4K9FK zl(fIz5~I0(GYYuBfsB){KCOhzXd5_-+!)-v#)vmPcJIPOa{n%JSst9HIX;lS_0jdW zU_3tzRXRRa%V!942z;(_zsR|1!3ZIjFfv>fe4o0r7Lg6n4HvpPPT9f9CTxGoZ~bSn z=2O)C8jmh_wi*&#i29!Qhe!UXF=9*D=9uQ0$X4PuNpxUsJt+8P?YQTrq9ZkZ#}i=e zm8-ecO_4VJGqUmnpxcsv*u>t{obR<7x{AP%(A?0GjYQ~tT|p=^ld@3r!O&~_R6{KT zjOW{LG{l}iehThX#Vh6V?b~dPV!PCik5Rt$6^68!1lHal;dZTH3`?kumFj`2;1#q^ zPT@kD(YALFXMsX*b)|3VmTaW9(TUuSRh=rju529x8Azy|ELB^c2N8LP9-9ZoI_QzsD|eLClmb@TrT4||KfMD$4!@AQ0|%Rw ze&Ai7$@VF~n6hk`M@WGot>@Yx=WQg2T6D$y@j;lcpT{02j?~BMW*x~I%i!x@{xh9@ zVdfv#FiOi4S6AQkBn_r<4UvELZOcwIP!9!Bf%RevvrSX`&c3R{dBrzSDo?>|xjQ;i=Al)0 zF?2v7y<7TZ$$jCRIjDu0zNK@!Kg*iZ0~o@%c!iXm(C-FNY#04c@zfo-n%drKFI#iO z+g0^HO*isVPhWkW;x94fOUn18Y3KxM+Xf;{1;G{#Q-^HOin}BU#QR~t!MyStr7C9S z@;6oHb5{6ii%#6gmek_y2esX(;+B23Vvs8pe`Wb?<0yYUg|RKCIJm)7K3)@>W&USB z>1|e_BDQtR`ur971OqseHwg6QWfAp5uK$_?JZltkLv`emXU`q#5!aKST|K8m{%NV? z4tYX{>5ux7)3}tQ)`S9;V=jfDRcvS_k6d|Zk39Y$Ra$b__C}1gTv^t?QL_~j705f= zlOS8jX~rTMYiLW_U%a)x38FH|X{17Tx9D%iq{&tEb3|{NWy2*E#Ea3x zGzE6`L89Z(-ycu~xGUmzIjQOJ6GRwULz!)&dWDQGs)dXihMbj>_eV|7H!6Vg-P8Pb zls?Q#uQI4#w06>b#H5OrB!w+a=&(nNnVI=3uSYxo(Jp~e%-?x8= zD}pdA=F^)}Flpt7(t~v_?jGEPsdGLO{`L%!&s&F1E*SVYX0TebtT==CC9#b?7lg_> z`ilbFXC#Nvesii+*L>2_Q04xNsoa%tr1D#hPolZL1-?Y|yk90TuOqm}tIOQ+*rMV< z8%y}0qDABA*Hj+`jz)&A9biM(H5AExE7aWm={=Uby`f1MrZKfht6v522~>j$$xKY= zK5?%dU2VS2*L3X%ZqNVXrXubATE^QJ)e7kkV$JPwg&*TvofG52Qd=vBZd240TCn;LicY=8gPigDG&QdJo4r)GO>Fu&vjHY``EnEQbN0`xI9~)-het4h8?!Z z9{u=R_yRhwYsSUp@Zq_&Q$6DVyF!5piQbQ%CR1|qfEbpSU;d1cn5`x;onVc0FyhFc z`r=ETIJaq*-mQ!;7|*AcS5xP_(?LIroqu(3YSp(n<^AQirYU3>hjN_{SX+D;6TInj z4;n)Z4eXkijsrG(=%pBC-RI5yFki|Wpyg4I>#UW>^x68U_z!Ic{&Q`{%mBcH-=h+7 z!+bom-PY=p60dh>)j{t3)u;X#w~;095zIYf16@j?gyX1UMkr5F$a~ocFm8@!pNzlX%{o#m|JxIDfG&kkn*wqyhUs0er+&ifbH`XXdvn12v7qVY0l__UJ&QD> zDM>G6m?@@B-{7$+;!%D^qmJ5+pKyisr@N{=g!eCUE{r{nyYezom@DBI%XxjP9IY?| zB~Dj;@VmrwvTMK7uc`aP+U|!t2r{faS(S&Sp6tjW1^uC1&Nh1DV5lLiDDX1rc z#7@~ugj4>qlV*$HumH-^bY=QVT!Xi~MBvZYg%}%lcr@Hs*%MG)+5iv&2=^TQ3I|)O zBpeou=Q1=LCZGMCOjNqTJ&mv>TR;c= zLaS36d}sL2NMhLkj?A$uGuowb&x_8xdN|kX$F#h_trZMBOQ?YyvG+uTz}iqmP1B8v zMYAEgW>ME8YVWHv5E6~=tnhv4b{-Wa#GudNGOqMC4Ou9 zk>-A!guC_(1b%6~k+@vVo2ErG7K@%M)I^&q;!~wBCAH+a8+nETjY_k3epHzr8uk+( z_%YsB)W!Cq{xr|bh<=Q%MIrh|@v+0at*?5geONS5SG`TC4Yr`XyVq^(>$_fL_O`m` z>oo96}Li*t}=i##SIh>uS;-*V~2{$2XeZ z#TbQdj-v;B>jIek9kWDwH$`$+*b2%5)~{@P1xdRRgAtqT`IJ)FhK7E0SIanjheGxI(KsUWViV z%GfQSDGlev@l(J%Lbfl%JF7P^B{$k$k*5LN8hK=#k!vlRYDLJmxb^8Oh|%hA*jiS! zSQ!}3Yi{xaq?N*HG!T;b0<`106Y&HNZ90uDp4K7j;rLy>6Ez=2%-epQ!wxLn67h#d z=VRUAq}BYqqe^Po(j7^kQ69w$7-Pd;c5bHYhm9ss8tY^;fK-?l2AqjjQq0Q?)D?)7 z9t`a%wyUAaN8BO8H@1!P91k-> zi>o(-J#Fv)E*~dlOxCI?03IgUb$*pZM$l{nFXA$BmosfIzC>*M^h(C_jHn8#6^k54*x`_gR;y&Umm_yB7I3{^c|<$ znq;CNwxLHP#3x*#K})g({?y?r;>JUXD{o(wuw2i<3GZxhiCw2RCUdlf7_oWNrM0Xc z&3$#+Y@pV+iHc`F5RSk61o6uB`WIiT_X~un3(pJq`xJ1043*9@eWGlQ#EMGB182=v zKp_e$TEE#GOE-f~{1-cZ?z4Wi+caf=^wg#~uG7Y?sltE0S{NH8Dd`PCjY~Jn|2Wte z&8;8nG?EP>IOyNYnpej4pz5|dQO(`e#q>=Cwmi2-AcO7?v~R@wIi)3AHzIO|A~SWN z2|XxCHZxQU&1b5QiSNsjHd9!!3g?|GKiq7~6lfzj}>^XJ0Ud^?i_5s!Mr8 zGB%vdCO6$n__**YKmpE&62}&1(QXOxS|swcoM!h!6@iEDAFNQpWC)#?5=-8)ORt3pD?j7Y}!1jMkJ+103N^U|5O{O zz^)lBSw5n6f9i%Q@a5a=pvt~wKNp*4O+EW_1<-0|&v_HwD{(pZuAiCATIka0tVbwF z-e6)IPyfMbo5)rBhq){$zoeP{?Z!{hgCHM{=c_;EMyK_-d|A)DOYdeE|DwMbKC_es zB9l9_l8d$p3>#8|RGr#!*)#Y*V8k!ka_k+yaYYR6jOK@ zvt1J$y!vMyw-S`&^_!Ob$K#Nvextxc=#kfd8_vE*v8BD@4V~;ZtrWT?_3ekvqI2s^ zbcRsU3SKdop~1UzD@>D;sjjb2p5IiB8fVYj&6jyq!n+E|(zTyyU6mn9^j~gUyGd*Q zaOd?~Pxqr#1Fo_;=qHG($b*rY%w=l06`BmU{`{SWJ*EcxRA2|Vp27}2m@h&c*5cz3 z3wYCwE-kquTlcL(@SI5#`^rzp3M7s&2OeBH3$Gj7PjV`A&9wEQ-{+lDx|NW|H5vQ2 zM-K5Lw|rvOR{$ip(v~^`Fz#P_Kv)NeCAj%c@1&oMCrFN1B%^pfsl?uYQU$srb-|QW zn4@F2KY3A$n(OtIQ%A7z`stI9HCZL``+J!~f8|8|F*kR@j9%J>2OtYQF^nU9=~f&b zD5sf6DtlR<%Bm|00X0t1L4Ko4J_ac(Ur!q;!#f4I{a)BfiM%4#`ySZ7DH^t->oaL6 z?Ov?@+0tzs6jgbnt|SwQLHqt0#U*$`)}Y3?=+jjdVO3fBMjy0ECU8W{6mK$Jox2AHIUd()QfhIbcE z08h-h&jQt}zT_T>Do}3L9ck51EtbZ(9JLHC%mo4t0(ZL8mYs&{kz@pgPSc}FE^~{W zZfDY@E8e#dNC!nni!4YRD>v><@y+s#PQpZbpAUM>DtM^gG=Vo1>spBKX$)1F6Gf66olCFH|80O8 z|0mn$GR&3MOkPvnwv@lN7-VUl*Y*9wAJe5VXWW%%2CtU>8l}2}ksz*S?Z}uE1?_u) z#hDIflY{~K5I-}3Md4sF4;H9=dB83L_cm()Oc~Pq^;h7Mq>@^$#pn3=8JXi>vrZ1y zH_GVGy0#BoRSTtDPV>V|7R;J@=6Qh$80~*^dR_rsuS3?CT+FUZ;K17_Ew<#*MwolR`vh*+)9Cu7Agl@y$ovPU$#wUXg?*zJ( zr2E&DZh<~1L}Jqmku|3C@XdJqHSbi%f#KxO+W;I(L;X58=3i}@Y2&}vvUW>s(AxT8 z4FzftA7UFUk2Ky9xb>G@HyPipjcB7~a8ft?n7jTLci}FrBNq+2s71OlzQy{dPmL6~ zI=<@GBm^YVVcn@E2xm4_&MyOY&kDDLoJm_(+|D$V#n7)yjElpP^W4I{)H)2LE)a z%!nHn2}m$~Zw_|E0nFZkOGume^B>(ulnkQ|SAS&b89Mjcj{|+)TSpQT-Nlb=rVxkB zFyuz=;t9w=GGu(v52(M$JBf9gxzATOH4bn~u@p%@O;#q3?e5c@$#}rF(`dGAkCa0-@tK3c88wDZvlUj@|K8W?(RG zQ0T7+`p{|EuzP4Lbq`I$oVi3*Z^J^2Z=Xpe{_ptgbPxMg1L^J|=)I*|d?t51r+t&y zf>galJO{WP^~>7BXDD0uGLtQha6h!*{Ak8lj!x*Lo-(HpK00^*KqV!yVu~hRrRy1h zuEuj<${IOVqI8v?2vnZJ4#N@NkjWM5_J)ulbhqq&V<}w=%EY%Ral*bYQGA6t^SC^3 z+LUqgs^e>&r^If*-dHVGVq|%%6eGRYDQ0!{d>-`G&&2a7xWRiWbgOx%zhzCKXx7KR za!&Yffx#J8!#DQb>Ipt&)`)XS?}Tdx!zyB=(E(G?hFqVk)dX8exH$3kPmAe9^Qn3l z+m?rK>Xu)xiNmWh`Y9t9iZ{zFRvU`NSH9xalRFFKP1PT~=Br_O-E(QQa6wZFECzW# zP|hi?xm@&Pdcy_0#K<+R zIxAYZpH+F*s*J8+AjGL5mql8mxte|Xn@3vD9=Q7}{8(Y7)W)BzcY!`sJ+&pNAS!_W zU|p4kbNBZT4E~9^!5*Q=tSFy$3wgGAz?31?74~Ca&z2N3seo8Egon;@x7Y;Fgec$4 zQ%dQTz2V~t=!B-~DszUz&_7hk39CXSep~mb$+P(4W#j8qa)f=_8l#%`~uoCYh-TqqpHVSIgGL+O>qqAs8euKC(`rf zI;DTO=VSR{M6r5}ORiMCs&_`j*TZ>Tf`M8ptA zrJnH>At+$j@|UxHd}2+3(&DKuNKS*!!f6m%poPZ#JQD)JtT=5{hA-TwflvxVd%e>^ z8qq5Dii{=Kd7mi*$Tdw|qTaJ8q4U?e0WxzT?BUP+(C6F-FR&%N($YKi}_yhy7xI1Ue)CQYsx1&)wIz)w&oHckKh^y`B^H;!sOl zo$nUukwWA)ecopN;;QVMqGF!nQeIv4o@pJ^sBJl7Nn<`gboUYiKBexktS;!1km-?p zjn(1rlMI@{eY+08aX2xd{aYQl=tn@m@u|7b^PtLn_1 zN(?hQIoeg}S8i^>tjP~b8c2MQjmynZ+vSV_v3*tueE_t4y&qSw^Wx=O^i>)<|mpbpPaxMUya!JHIlf zWy-4%TM_V4Y+{a6!g8pz+P3=@X&e9!$^iN+4rDeQuIsREN47dVaWa>jV%cMjlkb1!l~!4Kb#No3*~LAOtI*ds4^_l?kY;tCw zD}Fp9+ToQSEFXN+(q~y#`aXAdP?iT6fd3-BQ;95~(EwZq|3#dH?EIj!pK zl&iwJr68LG@PXk2d7gTec!(>_<}P8mS!tjsBiQPyBD+b(cA?BTvvSaVQ}RQ1&rxFY zGwi`=MuY9a7dRNb2q|1^*ABXWYCRGgzjol$d}+|4mwS4e*=HtTc5Gv6e>Jmex6C|5 zs=U6Zkd#HSjmYhcY+2@f0S9^{GREM{8sRb20CsuMduO8rGG%|n< z)fr(}iA7aGAE+=PR}WFD$Axi6d-o3M*~rhO?br!`W)%rQhP{gHO4KM%Q<>XVu)tnv zbs}hk-Fw|DhCVpW>xx}lGd9Hq=5x=bhXXEsWQCkMRz+KafTI(@rLjd$hNHWSAPZ`2 z8I_&R0^-(l$V7MSDvlOz9A~GSLNJagY$~gGY?<4p1ejevTtW+DlDk+(Di9!1>~{g- z$xvP1k-%)&%VDOA5JI5*)Rv#kcB*4KZ7uS&`VP$|27bCKVgRE_4+NZ|0$NbB$1g_w zB7NFc=JtwO%{F@O`mHN1R^2lGHDz8kRplVw`+!~_TH260sd4_g^i;5}z>jEibWZq6 zZ_6#}x8HBhF~Kg}&2<#@+?&n3(+L#zt+BS|j*G6*6%~7hbU^k^K6!{v`BnH_lTqy8p!xvHIS&!9C1k=57Dx&>|hw+?_sk z_Uxw`vt*s_QdD(trrZY|Yl)bm;O=KX^1pwO^QP&~AC2_P@zwYaiXx z9Y2+Mk>xF0GVA|=BGisPj4tQ`4(+{L3s{dJ`KD*9W^4Qz7eeeGv*BV#K4#3ax0f(X zIih3_Mz&t`2$ZY%EcZ$!V*eIwNuor0#qjlJR$HSPs_$uKs0JPMhF33?Oy3qV6B&4M zfrd|G)Rp^15Wp0; zWSed5i$7jGa(((ETbF<=vL|NT`{P@A{6Nvd*Av+#23%rEmWf=#Mzj1J;*U{lx#9*( zDa><}ah`bP{*zOrS$s8bd8j=7n><>}?WS-1NTuR;HU(9cwJWzlw>%hTITGqCdw_M? zJHqh|j#}q`o|wy|X%(xyG)w6c{3K2)87J}i-EsV#+<6*)6}nLkS~Dgp*=*I){e&n| zTIoF&kigA;=7Gvw(E122Jv_u6bTo~6^h-kLImpN7r^U8-I(Mgw?WnltKb0THd@7Db zhOaR%g6-1xzfSR#>B?@s!kCMxi1|p$6PDa~qjV0t2Irye7%O3uIW7A*%vh7Z){Yb0 zNYm>FCRWa^$iBy2(bYUf^@dRpwA8R!RG=A3hS8~>s40%#6xdgd z3sT%4BZOy1%7oYW)iU$R58HzdI{nbj{jnh?2{*YB+hqH1ifd|$pM;Rk)3yY95$}IS ztvz@&tuZN}T|T$08$?|^GNdeQz{pH6%ooZ&9=uIPucSGT)LvIc-sD|j;P(Hu$;|D$ zK9p2<*?s9GCqsUZTkg18M?a5iNBk^+G}cMS`3L8#XM?@XmQL!C(ls^R`z8387@H||cj{W?LMSwd`5+p z5o!kG!n~H_m@xO8n46HB&fVAYJuq=7vo(R6Yg~$n`3wGWb_0t%hlNmUgtxGxuzXfD zbHj?n5k_8@D8G<5g!v@Ih)ny#0$x+axeAtQ=BQvsXoR`}cCI}Uh1La?;rFTo5eq!I zS)hBkVuP5oz@Yz;68ODN9ceJ$6n=*q8SnNw^gvVxtG4yq!kJs_4AbZW&vE>F&uT9{ z*pT1rz@mp&gLCz53HA*X4}bSsE45smDbKF_P&WXsMe|qX*GQLHKra#?Zdg zvDyPSJyRTs@*vHlnVuru)Ug4p z!KE5G0LLR3vb>@paY$F~LFmkEQ9XaSx#CBjqiovUlOHrs{g3rG$xcF05C3a2;SK&D zrr!Fm>97s^7g11&sYsWIgdi;qLqI{LL>fjTQ%8-4ks{sF4IYH>wO;Qaj5l_-OB9yMXsj4_k0#<=ESSz$RoD~G^dOQ+-;(% z5am%q@KMipYu^OdPPf#T>E{%B^aYtpD*rO$)R_}fD46WiBar8{p9N;72>C+0EA0C9 zBnpJQf+zt6doEV8?|xP&Xzkqm(yqTJvKCjuj&%=saWK;R1~~KlH$aXiy!R>h9-x_D z@A*KO;mo_M2B_xGPj4Lj%hz*ek(+!uZ7j7EW?Wi&l%~4Rg&^m|1_zV>473^IXsCz# z!FcH{aU5Zi9|Lwop(fh!oG*3$p zJ_1nK?NE1<+e)LS3`QwGZQqS24Mbj zTgUc+dt!hTb0H5k^pvS7xa}yg?$jh56@{Slf=6sr=i*8TbCC~X=@9XwnwH2JkinBWcdz)2zo|SQ_pXD&}JwoQHO@l0=25#4j zl$V+pYwe(Z@M|rYF8OiANAroh7fc3nx-`UQ3p>g5ez`FZ zaB*Q?-@Sf(|LsHS>^Ez9W#;RJm_FZ39(ShaVHH(b91+i>nbZ9hsYJ=c{`d-s%e7~0 ztKr`T|L#oi0=5WMN)w}{3S~}Y6Fe%l`C`@d|87s9I4G}NwQ}2@^%o3ir@C$5eXM|1 zO1GD#j9`y=AGA)I=CF&|{R25kB>c?)=eF#viu+6p*>WUG`n*b5yO&$GN$B^()K$%Iah%vMu0DR__n5_JLC1>|v@|It^C*Zq??xT}!F4w4k>j+ftNa zYkgNm51{F+H0R_?>X`UpNYdh?;EYogsNQj;s}QrDR`V)_0q~-mu^7H8AP6$@d)(A0v^=Q+Hhq6-w8*l=jIl3-8^*d@q-xAnmoIkf2qsPhI z*}lCOm9ik}JeVcf*xxH3X1`gr&O0^N^!3Q;GCQ8!6bRB4y4!0&C~$xjA2uK>Ztsj) zx2%dpC^=(AZKhsL)PazZHxVru(t3O{GBCb!1_?A}hAlGq!g8^wM+lyt9XL)%>`cX%wmBbIm*X%*Bp(%h3hP{OEyA}gQ+88qqptIcYd?} zdX!y{_RytGy!+}xmFGwUwSQ<)!sQv;K-A845DplPB%c z8ZD)2r4x7ZX}%ot>H&fV_#P5cv|MSxs7Yj2-i{W(rzxBl;aMAojZ9l#pRfyecZ|9P z6z)BJNR6^_?p@wi_-*qwfLB|)oa<4oZI3n`1jK1p7a^XvbuEFU$#P*h*?alh{K}25 zBGDYIMs1h_;6-bYYgw2&bBmf_@|3dRspOcx@We(}j9VDZhzk4J)}a1P4g~hVVq%eo zN=6gx{7RmFR^EGGpl|>bRs?I@sBRW(erCCOVAKcPGv`?tW4zKnO7+`F)IpPyUvyUC zBJ692Q){8yMz_+&m*IIbXVtf;-OEu}dnx#GfA;Fb%^ry~JwmRo!a1#I*`sE(-c4x^ zncZB9n?>EXIi#%FelYTiyK6v6g-{d~c#`i@qVW!@hi>#BD|G95JB<`p3sbVrpN)jz z<{U0Y9BO)x{Da-vNsKXDSqL4ejN;3JL^n@Cqie>|rl0FiuHMqzUxK5q7m{Zkq{~`@ zd;C5~>oLnA8|d-^zw~)w+Ivl;zrN~~S4P&&ww}JF7QW*gWyK3ZoN0mk0hISQAZNE`9Lpq zgX4SCVD_;Loew3BL)72dg$a>kK}rEa zXY1^L4jDIom+5|F!AEnGi^{D)f|B@t=#eN*Op0thEGeGUx(3NXm~`WcTQ_rL3u)z_ z3nUNt86Ic){yQ19LqHYWkoKJ=ceO=|3pNig|ew^+~w~bDCj1Bu8*%A zY_Rs-q*0X(;>lmyuxg8>uG?=H6bSDz_?vTmg9c04R!1_xL;5me~PO)FU z%9Kczp*T&xKg0d%``XfNzqy_sfEPMN7gq0JD`#rZz0(99F|A_uhyZk@#)F$)4Khbv zrOypex2(<>H7s}J$A|b9*>`I@XC#e~DbJ=(rV7j%!)(ZD2YR0I?XhwY>3lraX12uT z%~EU;lEc=Hm!>^Th&N1)OO^oi{lWn!Gy;O1_tr)eQIq!A(#@;w-qwq=h>IJUJQ-AD z>&6*AUS-|z_Y%pE!uk8hOuDtDq5i640`Bc%d*Rlp57&kt0*&Sy%V`m*Z|Et2v@Fkr z7rnP%Eimg25?AP8t8d2FcD`RkD3m}P1)97aj>cdzuZ331aNfy?E~mx9{|&~i6&GDa zugLD;c*Z{#bBj)`Lnr>UqmhmeVeG9jHyZCX6ej{#28LeI@^9%}vt-;KjzMm>E ziYkk~wOy?6bzU&=L9GG*-K|-8zxJRgH8%ER4QG;7X`VJg>ND|w4Dqt^O&*WBw4CBJp%I8+5uUxf08rP9Np>xLY}X9r&pMNty~)l?`6jH)dUO7I#j8R&h-2vvtMHxjvdj7;SyL}J_+a!i_pPP{`JSg|0S``MxT(Q7 zhH!$00zvwyX0Fng7B&R{#s=6@{;~=G{<)WXWS${Kl>AM*gFxPSrqY~Zy{^njvO5|! zyI;gLrenNG0Xh0LlU4>>cYPQoBi!KCZNM+Io#fe_Ty(Zhu6Lx}*Ppcj0xcI4;U0y$1I;_Qo$E4|%(mCep*ACJs9&D;u2BDF;w7mDr+C%+Y`HI__ z*84s&98wtGX2eyvAgLuKn^Sds%d}^H5 zVuwHt!hr-!TOwyvF3txqOUVN_rgQiGk-HJW^%RJ+@s@j$4FtWLQ!ktZr{2@`F3@&- zxjf?c*HZO$DQza)mIwjd7V>!@7z9!zS2b*n$L&mqTn}BK#pkDc{yV8Z;%uM?$#Us1 zyT59AQcIJ|mk*<)*y5Eew_rc64$VfAkBb{zXr|1Tcqd!FGxP5Lk57a!-)q7vB_dd9 z86^svuO>b3QJceC=O1M{*ra+|godu?R351LHEz-hp!*h zLeYEEd|yH>ot6H)I^jH;hd0e(vw)6Y)Aq@5=+LpdXm+HI1KFbbHNWWSzie+!r1x5= zks}W8$G2)ASbF=gjc4vbURG~C^Iq0aFHAog*CuPeFvWIqO(2NsQv||QjIgM;zZq9t z(GB(oflL5*eklyVPLLc{)(#&8EPFkse$eRfPWW^m)AM`Abu!6yvtNU^B&F#b>5dB5 zDJw%4c`n_OU~hdRTd-&Hwpeb|47OPbqj<1>+Et8yUH=_5S(Kj0-oCfBM&@QL_30hZmYH!uN9S!kFwM7~No-!la_gbiWxEaVPNA_t=W1ezPvn^3*|O!&El!`w zq4$RODU3m}8uw8a={;rOXd_Lcq3HaJb|JX3_(9{n#RuGe8yZ|9&$hc)BrKFsCnm#o zQk&nwCF+++37~#M4;Gh*p5DdVc=xN{N|z7aR~MS@;Q8#T2`wMu^?eV4Ma5%%r2bXN zE3Lk5>eB0iJkL-<%Zv5bydrj;tqz|vE2#tP?#hV!R#+;p^cU5wA*7!25O>FWPrqa` z6y!T904p=DxCKVIQfz?6q#teEdLdF&k33ck)K;F}er!u;TB!C9uFAl9KirVwDKTgG1|c98587I)uR{(g1p(8t7+JU-yAkz zAG&a{;%0;(UihC(|E8DB?WmRDaXsVGBHS(aUjKs)6=%;6q7i}T?J0gtb?CPli;^-_ z!VHsMlf5T4Jx6zM^?(U>vK1Stb?d$9W}ZP@q!QTl?oKMc)@Wh!JQ`$!)9iZn`8;MR zY;wqD(jR%s^j#1Jvck}6|LmSkPcdYEk}KjVpDG08lXgUacqws`b{_cAYZ)wkbXMj)%q)#d zDZ>I-t<$H0uk`#cI)&!5ftNQaHwPNvpTeEs{@I>>iIKM@NemPL!tpp4A=N=~*_?`P zhlkXwjEFATXOR6@@McXK$8$2%$Zjfpzvu>UPQm?_oX8M|o+;xaQuwf_mfEv?EeFtdYKr6i1tOid|ctv;quD;*%-C zv?l`vou9jIO?JW_HqqR-1eno#;K5C3BTyeYFu)VIz zO8}mS@OC?;r^Srt5e4#N)fBgMd}_!7>mwmYjPzpIUrX|!90NB*!{y)%(sAijuhuNt zHkBD6g6#C}R)sCxf!n>F0S~46&I579Le5}=Jtw9Jj**-UcuC^6_XA|>4aXfIWv%)D zM1Jy%$D&sRJ~wo2_`ET9Yk=OM@P&NaC7^jRw6jip;VZB_H}YUIkT`y3Hkww$7Mtq- zadjkP-r4p;b-F7*j@`W8viDAzz*#Za8WOQv-mA>Kz-XRUk!@bQh}2KK2h7{^o(c3? zJIk&0ocWJDBWjO%cN`(F)l)y#zWiw<`}e2i4Nqss8YIJ>^phv5HV|;>()Q z_3eXLRK5Jf9^a6dMQU9HR@ywO7WY0ap)k+^@b;$Ds^akc`pY;cVpUmCVYz9mATvjF zE-?}?t@<%TJ@jV?r7R~bH?P-Zv44* zX zw8r4-zXtGyd%xQG8tj?FIlSK8=H@ZG&51s6XyG0!anZIEOd7pBQ#C6&*4gU1YtAQw ztMJ}Z%P_j|FPj0DIl1^NF;`~*)dsCHf@Kla2UWE#0A6X1T+b^NBV;%8f-0qv%qIEXk3+UJXbZYEwyu z%}MN8h#1@-2eAs}jusD!Y!}FMki@)28QyaEyxBM>ZHYDN<=OMG$sg#oO>Ov&l?#rUeQh)ujpJ764&74LinNG+lbi+>s!1bGcPno$((M8_#-0te0sOu4z+s$3u z>qTP^RgcR&N3<`+j)NZJdu^=EEz2?@tSVG|w7hI+F*M|{H2K>bd^|894XkbTvpx#) zGV=1XC4*&WvAW>SV3Y%|i1O#mNB*PjmO?ec1Z0LUE57S)hZN-3G<_zQ68Gr`v6Jvl z`8bhY(`wy|X5=HI50r*xQK9WIgUeF8&4#-gc=9CUu(-!6Y%bCo#dTlBcTP~w^8H8( z@hi@Rf>?2fx6jyR@AKXsKLMlIyyrD-gU*Qc>LY0W2&oJ^2pZvdyq{*^v`M$h-R4W{ zvd-y#eH}d!y-;8Y{%yhRzWYPvW<4vHjaYW|eH)X!>q9NKS=vgX04=>Qfu6@^gi6Y5 zRJZ)!>qNSFBIM}@$~&v!ucV+{0sNfuGptr!Kfx$zXZbhL0>Hl#gLhkakVpt89rm6QF50>*TmnR1e*7JgNauCP<{MuwcW z6@iMhtzXjRlFB=8%zAoL-_J1&-?!~f5|}rN5F!`fn7rNsXrg}bDE~i)?2Mi$H-W{U zaFnmo`3-S0JLC#ZvAMFhZ1_|!pPo4|U}v4vY!!)1ZzO6rWzB-!C`oYk^_uT=#PGTZJYcSMw);8G%I$&b7 zr7tnlccOm_??2Q!$hpw-3<cXJ*zN)z}TuhW_)C#oW{abY70mm|w?|#~dpxw!%sB2b}ZT{acWx|Ij zQ-wcK5i8Vh;=WvGsd~^));{nEDBKy!={u{)&d;IKhX3hf|C^8e={pVb>h2{QUAc50<^rz6ob+vE6!9M1`KH~Iw z8j1!2LFh9Qn#WTSHVEv2G4l}E-$yY9)wVkH1ySK_Y}$DVw7P#2627N{cy=`+ekNJ$$<%&XkOzcbg?O*kx_qr-)-G(F zzJQ10szz|;2kES^GAWpveTzhPch4>mWJw8Si0sFk57uW_^Jdga5S z^Fl{}`|X9Eu@0btaEAGg)DENW1|I;*RM`?ub`ZE(o|Pinz&sMpgSgCmybL%hUsyrq zmN7Bd-Ai8+jT9n1SMS_&>8Eb43*f^86u`=j_qjQrWb;XXLb?~upLv1`_=DQpZGy2E zT6Wh{?JRt=!zq$B*0qJyj$;d|xIOq~*}y-jTG({sHhz?|ujy#w@r=(|@7I%#_craN z!Xung)a>ySnB}j&#ZSVVgR6J$rd>Fl#D8h9ANcs!P9sI}!+NOFtcEdKlAWNUJM7b& zhgL?oaH1^v@TF`d;QdvM`HRGAMSZZXVIVTj(89o^*JN9ZHb)cmdftWo*&IQ|umk62 z#xGi5U~t}_p>MojWHRpjtqZmj(+Ii87J^}2)}0i($OEHYk?IJJ-%XhgN7{`r68q|; zjuw4l9c{@*u}v#&Ss~u)(nZFZdM*AfiOr`ldn-$YQ-v@qJ^hBfhfkf2Hg#Z`KmaeQ zYrg7QVQCdDN~Bpb@|~I@uT8!2tS{d&T&cUpc40sf8HEz&JLR=4BhEc2B~xy4X0=-F zG7xlLQ@Gw05|0(S>2xtG!2T||@I)Mc;{#scFF4O-C2tMtIcXYCIfN%wlxNVmZpq2( zZ;=&jnHLi`m~Z%utVC>hskk;NGQ>J{Lz#E8YLUu#O)>o3w$)U=_A7lK1ouPdqA*!8 z(*0=c{Yq2qAv18If>}6>VDwuAW#<(y);M-%m+F5bqwv4 z>YnmH6M&5MKkOEgd+k@QPjgFCYVkV|bDa#uI$;iF+*P3WYthRmO;;NMBV{)Eoc4_>BO}5W`krzlo+X~jGp?Olk=I+_ zV1%kFhms6Hk``M#B*VWTk}NTq2W$?GQmZd{`R2@jIJ^>4P29?uji4Qz&3CKcsdePiL3Lt!0V=v9nTthbu7 zoO-P~enfB*dC%GHNXlVNLMH}&enA=`MH?Y&v5}J^^2405tUr37l9Fg?Q=3pdV5w<# zlEf|xWh@p8ICt%@wcP0v@C>b*{hg6`H$ldp)Ui-LRQGf8zINQ`@(W4n5lyvu?&v!uU%cNDkrH2R zE3)=zw9}^gez{YKP@QDfIt92}0G{Y!u_+HvwWb%w40nN&x!Jv#3cq(9G>ua(OPCH) z&$-wAD;1{#iy%HRmG(1WAYoFe*%bj|4tql2g&~#e&(2;s#DOD=%}me*V#BqPl$Jht zN-sW0$kL%SBlybrBF>v1IQzRhTYO5$nf0o$9faP7hB@tgA%ABB+C7n1Zmg__KE?C6}E(9J27iUJbHe3_EbqsHUS&N zWR2>*2QHB_Qxh~&rrSj{lca=H2viJY*=rw06}DKYCOm>a(G1s*_FesSQ*twQ%t0n@ z+s}pC_dI+eRV*BdDc0(pm{F=*-dF>tfjwx8-NeD8v(Kvd`z8}!PeVKiW};e7hCX#X zrfHT-)$N~cD+3_9PBcH6ElNiSHz< z#Q`<8lRKay;ID|sBxgj8f8=o&AZMf9KixN?IuOokgU#e_a+>_VwKu~X@(ua6_Vjg% z(?NoSTH(x$1?kSVX3`fcS4a;@2|K*R>0mZ+=2`LIS{Ng{X&4( zny=V1Epj~}^3vLGL%%h-E{*K!gZYpAj0)nR_}y$QX_!|-9-mu3w^~`yE$ON{y@aH? z=1hqYWt!Pxz&JJE_-Q+vCI4A*@{1YWT`TA8yFx@#OPZVZHIdu)udc$_KoS@fzMLA214Hi9VNF(G7lPszLR?Y?fr0wR;A}ybi%f8N@K(`gD6?gU&&G?Ter*iSRhN1QwC`lS2G|>IXAJuq%l(IN5 zBOW#zV?SV5$lQJkqQ#HJ6Vh5%DO)d_4#x=_#yty~p|?w~&E96z$F8;;^6(ny2^)2f;dmowM8UEY$GI2%ZTkQKS zFw8Puwo9iezFnN0^Z&1Y_mM7UmRZK4cNwa~0A20M*MHg5-NGnTXEjOM;vo_6*bS;` z&AR=slbUax6Org5P83h)iuNJ&;EaX!%|ILf_wCI`wwR{I+wAKPEr5aizffcbqPe!4 zEtjjSmS;+X2khyhyuj7A3l*m8?X#v(67MkhS_;>&qI*(kZDvviwZLE*96zY&ks`(3 z?j^L^C}CLQ_gHJVR;5lv@~YBOKHJ(f_1E`FVHY{*1ael+iC!hQ$k)X1e0mXEmMdu6 zu!+_`D);rJ$UhP$4rG=7(y0X%435&C^lL+*4A6CTD3u86k96sBTZDGr%f=*R{{ElGllR zY$GGL=_&Q$cX^#oad!pjuW-8&*S%f>V%>l_4!{YB<7B+$rpEgQy+m>`ire)}#~N0` z{M9bPq>s7JByYzRJpRdShbJ|Q<{{2AS~~MCj>#*AAdj{X{y4lVj5ma|`QWAzk*IVi z{b<^H-up{a#KNR6epSb2)QFQ}sTfLu-F9|w3D)rxX9H5Lw(&di@M29}NSRs!&86O1 zT@Vth)+MHCa0=S&S~)0hsk@2Gj2?&D_WWvv&(cOz?+qqmI($uc^g-u-iXapGi(U!o_ zqxa6pnnw9W(x8hek4J_kdiAqn%6EcfMa18bkg7v_QtLmldFZB=TMcCEGHBdjdbqJ0 z;`Krthsbqm&*?A$QoDzS!Q3*hnC#gkV#u!BViqWnNlBn^p3oZjN=0N`9#&rFM^n`@ zJ>ou}XO)6HO-_;(d*L#Yw%YT3WuV+4t_<=kOx?++xr@7{Qc`F1^AMFI=|!LsY1m1? z;5p0DNls~7B20&`Wj;2nB+-vJ1;edys?V$&IWl}nYQ%3RNnD;*i^41IsKd;Eh=}Y5 zqBkP>>Jm{dqoR9Rq8Z`B+_l0AD1s;8ONmIlm-Zi+jq>=3%yEG2my$i>4^?VQzbZJY z)FkMq9k8aQDuTb}se8LR^n3|bN3Hp!L>#37(iN$lshw<>mdfi>bdsGF+Mcu& z#$x=WhM|LA$3Y~@eSz6#lI)2P+(OHw(ow~g4z98kk#%vye2y*jFu^PI)b23rA|na0 zveR()50_az^8NNQZ64X(Ly0spUFC@jLb`*y!InpAqz-*l)Jv;FtV}hZ&-_eJvZxie zT27vHc3!$#FNA%a6FD^i+{Ugoz%#I0%dUW(7{{e!ug}2TtyQ@U z=mm3v__V}ns>ap(hnK-VmIaxg6B1popC+Pfym^saPy0g%IbT$l!Ru{xn_S|PDF2!v zjN-}DTgtjPU%Sx>Pz+7nrG6!>kfSGnNfxbl^q;}(YoBLB`mVoz36JZ3P!s&(3llBZ zhjq2|$(M^<0}P8PB`$GgtNn^z3l3Q)?S_j2@A;!>vj)DaoP3S&eM-$hYpnVOag)7t zWlUpEnEGclWNgVLi}3C1wL$?sR(J(<{m{G3@@YFN=F#;I>-m@kVN2rNCCqNdLud83 zHBzsYlAzrP%u?OGHnYPB1N52KDuo2xWRs$522*Qmx=$Zq$ftKibRLq|Fu)upp4?dX zto8$gf%uxiRr(O)qf^r=(psd8&2XeGsp;c*3H7xNhJOlQeZ}`l1sE}sEvLlXfa@R` zj|RY9%A;~g+YQ~a`n3VO;hsM!Cv2owik=?liZY0^l-5<@`M^1j!DWr3aP*n&z}fdh z0+EYZLFdR_HB$2mtjD))P-aopd!}!jd`gB4WhXzRbf2FC#xFDBoD0Rh_E^eRbV%!_ zbBQ|6d0~_Mg@ntcNkp#V4Wq+}$L|)9LG&mj42PM|o--G5y*SMMY!< zo|j`;MhM}6t6l0bhB)Gy^NL|{7^9HbmgDo3PP=Y%g4%k8H2hW4Y??t+3<LRjw2+ zGQpM7_XmLF;s_8fzI=BYwht_NE)OTsT~ohLkU#^K`A~`@35PfA-Wy8~p?8FWi(hnc zoM&h;`5kl`3tGI{@9^n-Zuc$r^EZBYn@!ce{fBIOe*Vz$LWliiIUekDg}85q6@z>~ z#Vz4cZ#NIs`u6WbZjy=%SL%I8^qhI^x|Bk6Lt})AVln zeo}SOS6VEs85$o^d(dkBgPmCny8mnZ>wfV}u|G?eT^omaMBOJb6y{2pW+0DWpdkGr zO{KNLFS6Dm<_z`Lct2uw;ZI_HgY{IZPI2T$y&TNl{((t?pE}He-SELGR<}(31+GQ(qjZ5GpeNdOG7)a$530Qo5j}4tz$tFvUs_ zP<7TH7(}jDgi9wmm$Y{c3RtH4AljJttPac#cFKndR571REAi&=CHP8(0LYs|IH-Pd z8TIvWYomO2oYojO@EbM@-qurSd&qxVi;}qz<#&KrU0bs3+N}aD;28hd%3Q+|6a^}X zJA??Xk1h0NBSV{1#*cGjq}T?#nVYy)EkE1TeSU&at@^<|K06}pUv-sbqG=tp$&PNmHHm1L`1u${iZ-_c-&m`L{fOo~91-=)krlVQtr)^a%?WcR_iGXI zf8ScUU@Pj#5iY|@+lPOSQX!vyN@AK*&Gssb(&=RqLdAqZFmBbpz>{e*iy>*kB{T%a zUQG9?CSu4kV!&~V2WBc!_ZHS_h?}i_7sEUTejhqW@SR zbzhjLIJb|*8A4p;dmkH3+?bJnj=u;R%YM#>iD$_e=)UP7hC@Ij9)slDLp5El7763| zE4OHGv5VaE;8WRz-C!e@NmloH$#)@CW>CZ@^8~) zj@bGjJtzrBBpcv5M^t2coAkBFw6>rKBA85%q0=UVA6*B(H92n;UFBK~AqU}YEAVn) zgQs5nA2k57(r=Nn-=&3gtlCFzX zo0gwFR2srW0Ld75=!NClK6@307aNb>l;B)HV{hH~aDOHTfvEM#KqHKR^^&~P+D%AS zBiB3Nj~%NQh=b0GsXk%mp~Bmf!O$Ctu8Gcggd}7Awj#~9pU;e9zf);npqh;3$m7XK zm z>D5mjBb1_eaKd6OX9k^j?_Y<3Z%)=;>5<3x7}4AcBSD2SalsE68Cm_*pz4+tym}QD zv7+~UVmTao>~cBR)npC5*;#(y(=^1hWid8<1~wa`^1PNSR94Ua$1~@=FU+0e%_pQx zHa%UAQu{P09#W;qfl9Xnj?%j-UYbH$*VU!coC)cv_zs%cw^t3NRHK2EXPI--0Gp&$ zSy=6~n~@~lK6^(iNMxwY_8BFX( zj<^rBn1PG`TE_p&*pzp(UXZ&Fcdv17CyA^w)l9OYtFEnehIGY5k#U4Tl-Zj|g3;*9 zipXZn*;nH;K1=4N(dCWgX4^v+A;WC&Qm;N(R8fY5SUMcN87*@~7ve8fXDvBNHe_?V zN4h^UlpH8G^{Oa!HFDEz{;W$|)n5;3h&PpSs3nI5ec#Q#O5kk(8enBO%hq{z+VIO{ zwI&pnkMP1Y`E=1@TRC$nhf}&ykMnXR))|j)?$XSKIj8b*Im`{m;*oI+kn6B$TSE%~ zefELBll10FO`Be1DVjyCdE=u?iT8~0(Z>tk6uvOnhtcL%!Dp7|lg^l-YJ1oL?px{8 za_?P)7kU?wj!Bw6dt6*!whlT~7w;aakN5{a499jCx9nWUliYZMsW=M9ICOUHKxS#( z7jA%6@UFp+?wF#qt@roHCf!#a%7GlXiQxl6AdA68%?z|(S=V-yT3S)@tJzw|7fix} zHE+9!>AksX{&x6$-G`t;npUThR7_Z6I;+cHD3r4L>1QhLKC9k*%6j@vV%ZyN60wFm zg1zmcd1qYxzI6S!65U`eG7{Yr{W8g5lji3{yc+1ugF8ipPbZ+qi#_|%$l7U7iv#^c zq4QiJue1ZgvajI3PH0%q!TyT~d-@jeHRrdtWMbWF5q3gS2`W1$(ID~X8>VTe-NsiZ zcmh|$ob@IQEX#z~X||Z^ghl(EwQaqCTqSXruRmxEbmj1-nGgcEfKsTL7hdXES)eMn zDN%{o;y6Pf=ttXhV!OXvdfp*Rzexm(lcP|;Wk+*h{oe;R3cXx&Zdo(jXWM4mYXecn{gr`MUR$QEV=0P2 zF~9f5G-nYkR$EjN^oTLx7Bgqnb_C5fYii)>WCJEl5o~l3UG30C9n%1L`hUCtMk4%W zkQ0*Wp7W6sp@H=bU0+GB(psTAow7IbGZ-xB7TK~6u($#AYhmBtu4lxaDsy|iW58(jO%ROLjt~*v_SX3P86?l{j8VIlr`~2N% zk#j=})Cc6d6FpPWUB#I9HaPWqJb}9D*w%L;Xy8Xy=d1eq8I})EH7GZDIj%UE&7wT6 zSw#5soC${POl_?hm|LWepTguji#muY1Jfe?jSG_1=%X)XE8^Z7pEq}vrndVt z>xM^li^Ji_JL=iiLzt%t6`OF#0!19&GN9x-nYgeceOl(MW7Xi=!Z#v8P>z|ezbiCB z0RF@P8Y+Kg2Y+1ZI%J*VVZPS1P*nF%#9K0Fwc*&>&17ttntkghheW zA!{iv9ddevO}J%52evq8EKZJ%D;@!0!HZ&7E}0N1DN{rkza{BG(Z7Xm(8N8_Z1cEc zk5AgXEGeP&=4-~#uVtWp!bWsSQ6k1vh^Vog)h6hrOUZ2}$5PpzI8L+iQRu$c3z$?2 z)^_CoC4XkiR$$R==3nW(WTbG>e-I%~Y?Qr;#KOVzI!JB6GJtL$78a?|?++s=vgE zfA&J(`@BQip4od6(Yk$BpW&*0e#KIE-qN!>NO!_;$-g(-HU6Y@BRAZtDi_;nv>r?H zSt4qXlS8Z?)jToTW-dUJeGM^JH#cv}UM~|%FIE7WWRU=lO5{ml{?z@z$f~M!mS%zs zJM{6A`lvL1pi^S>Se)2idNP4oOaUQ_v5 zw-D)zyK~Ni%`)4;dNqmdHJ)pVGWFG^vl=2jQZ?!!R+QAuBP;0DahP6CX`2pPJPG?H&uGrx5l6gy-BzpBvyaul{>KwU! z?s8({b0lSc;3~-eyql05VOQeqrcnZPHr`$GoV~?kVF+x&rZF>;ahyWcI76IHyJUWz z5)pf2N}ugH)?Y*LWqw3B>QbQhqA!B*F(JYg>cT0y4a8=ZbXly(!2>z}k-hr%b5jJB z^&vuCW+X&~fl7IfwSI4%=01O(`{sMGo@7d>bZ0_SRbK%5-w(SOK!q21c@*khX z8BL_C7ws+m`%3}(8;#}$QbPl?7=jc%5K)FCLL+o*pGTI<;DKZ4Y|_-GUup(sy_6Dj zRY&&p!dB3Bw(%*l+~|!V7y0}e-GSD+@&>_vE|Vf=#tnXUhBI!K=>ZcGT-O4yVjXm? zY259iDg>V4t8JqtEOUBa(4F<+tG#57v2Y*b8Yd-C6 zqj7uQ2ps)s*S>Ook^~yD&g--#<)2*>G1}J8PH_Y11fLX0?1M?YM%=bduui}*u3U^hmQKWgJ3sE4YL&Kfy0oUati#b%d*1? z>MK58`%4@+`$V15Ur4RpzOoF_zQ(Xg?G6uv;A+9Q)hsiEga`lWWdQ$hz3mA3x-D0q zE!Ni_djz|IrOaKZSc)vTS3V$(DZkp;qC8^F?SwjuDbtK2($yC&UyXu?6W2k9(h&aw zNo^ORHnzqEp$19%9?mUVDRo>u`~B57i;}g`Nk;X(=`fik`K$dKpgE>M!Cpqgm*v zX0xFL>nW}NlI=_GyH6n&>Ox-!{ay+>zZTc4*~hS2urUjHZ{PN5R0_`PaaS}ms70r_ zezsDI7JlQmLL~CYY1x`wq;ZAPIPH*Y^$tC~n3xuN&&XLcl(*k$L^#RdrH^1quJ$xH zrGGa(tZlIwDyXFM1TDtJ27ViD7%(Q^aBV^EXXU1rPed3ZV_x;AxE*>|Z;rzelD@4*T0lZ#o}kHrf3eb+fB@G(FMj$?)u$UYa??K~N4^W>(VFZKv)vDF|WuRTtj6z-4Ubos0 zGwh@X{;Hb!;tX9orEB9S0@K+uj_{<^C;ckL`^j4hCE)rhcyouPT?^)r+PFKkT|LhS ztLL{qwy3|?({LX)&?((GksYH`*~C2_-m8DAqa zv`l9?{`HD{I<&nfs**Br_+d|s^e1Iyh?V$crUdK9ndyf`5zjw=-SkubmCE^vBB@Y$ z4z@JZrGs3Ndv>U=8(d1U^+DAkMxl-$*8ONiu9EzBo zS1{PRC%8G<+NT;_p;x!Q2$$@OTkQwxI4(cnLlHH|+)y(*yr4oViS;saF2hOnR1{5VR5XzF& z^|)p)mc=muiV^-~c5$s;BBSG(%9lylOMQpB{Nc=}V?R~~DVpodV?v@PX0G2-O>HM8 z1-f`}I;&t3CLG`UScFlo&qQScq)}LN%#|Q_;=8S%m?gAXAkolM9_qKagEslMf|}}X zvt``XMd)0&m3kNsJ|$^{Go25;Bzy%+gnZfp+!L9RyCWeo6RFaql`_M@&@uqA!7?5R zhgOMs(ahYU@??~_d2Kci-I&dFt|a6~ai)Zw#q!uE4Mf-{fw|R@Mwu7kZq^2d9Hu@q zZyii2_{$s&0T{6lgut^V)i#q4R3o6~`*E?H;fX042Yhnq>2^D^caLEp^n&b zlcQd;*nFd|k24?9mIpDO^VH@08*M*$|LpT%QsHPFSV%$(^23Ky*B_;?;$-g3f=FB) zt&W{l8MIPz>;z{#m2qJm;>$C?#12Rv(q){oGIB3;=X;o*x;bfclc9VvvGJBMvq|q2jC$G#U3oRfbdjpf!FBP^@thVbS#c|a zE-)5AcK0=fn0q>P7xA6_f|R>w4_4%68`cwv2S=)tz1E;DoZJ#B&D|?1(?Gj!YN=_A~uanZF?Sf+ttw2kkwP4 z6gv|$$z$B%wn;f(5zmydZCYr}uxZdOX&kd+xHRv$IlZi7I(UHKQW$K^wl1%2KfV#o z(_i>TurzRPYKpd_^M4%+JUkgyEo$-NzOQp|!bdvX>8b6oJ#5bq+#;Ug?hG9Z^@(7% zo_6s~Q2);Uwx)=_84=QgDb_a(St(Cdzk7#ytM%&@&JT3hBjx0C%L!vp&i@in z)d(*$vrz$F-4lNc@3J6iA3grO+8#P09rSlj41ofboTXb&t;>#ntw%mBV~3Oe zxo6*PDR=j=IbOA(pde>1XhI~SPB{G*6UAKR-It6n$>B=_*77Sx{%wG9eQ83?i`D!<5w~CHBj1_ zi>Wtthd&-ft(F0l$MY^Y5Cim_O5p+dg4DF%dRa^tcVSazUo&H*b5cz9dBunJZ*jy* zB0eZ_Xvhelt9UN8n)i(nJ$Hb%s)q@4UwZn4CpE<4y8n&^yd%kq7&{m25kVx-(E3z8 zqFvYDPmkP8wNt3%)M=M|2|)wZ#s{??Hgov&ty)yirg})=AW8EGaJZ!j@rF5d&l4sc zi1|%us%ZAUvhSVgO%}n^C*7}JsQxCGs^9ngy=#TJN6?R74}$xy+$)(1r!~ahYno&k zWk>`xei2T#JvRLHvQwd@sJp4Lj+)J9s4*+tQthTc6lvp|uh*IDYUqrQsN+BO6$zgR zJ?I?t=J(F%#b%jqrIB-~V1j*n%-nW@m$J{aTj8~D-y;0iH<0!hLmkb@+gcmDwBD}> zFD5uomCdR(yCOY@Eo@sJ+YGMq<$gWo7P*i?Z7`Owxr>^GzAD+#$~5nToHY$Uh--6f zgryiB21-x*A6y_QWYp-#0x)EszfFg8=dtuk1FyaxchYwG=$g~>JqxUY+9{k8kIp@b zqz&1L>C!zI2G9q3MvqFFz*Et^>xTRIA^u^FZ_s>#F5&cb5sMDtU#3?fM-NNV)#xeV z=!O#?3n5JDe4z6VeCiE6>2gg)a`{dah9vZ`Z-uv`C`QrSZboGF21-I{Bjj(mJRXNj zBe%My)|+}uC= zXWlu7HrZQ-=KV`p92BSqWyQ(;%tWi;K*Cq4Lg2@LLniOSuy$rIcAS_s^; zjDIsn`d=p1|Ada5-&&1Lt=Sbfak9+3wG-J7dS_Iv0cu^^xIxS)H-&0{3L=c{AII(w z9&8m5xnQhHwsR-9--rh4iax{czSQ{jy+^2*)iabjI$+BDA9kO9LayspuQSH+JRhD4C{WG(a=)MkWs?L-&Mf|N|Qd{^)h|?D#tk2Tk zz7t9iRusBJ8WwtJwmV~$RnPb8O78CN0r97mU-K}jNmhQ<MhPMm9N-`e~0TIS0}3_j*-(C zi>wag=g$LSH|J()vuz8Sv`8U?sr+aj>EMomy@naSpHlLCeyNkbbgc!7ae5@LRUe$# zPD(101OzVcQ+WFl5_7_-%0AF!dlRWMv2rDEQmxL{ZS6eC9{XggMu*h&$nCWiI15NPB zk4^Y+RAkETmy`C5YE^QdN=@iNup)~&Gi#ie@y#n&F=jxP2}sM+QDMwjzouuUxsXgO(Bgk1v0`4F%OuOgo2ydCQtu} zDloEH9Oga>G}m6}v&=wH!-na^cZ_E%uCEYZ{2CT_sbyUK55}sd4fbj=(t9a%0A}eQ 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 literal 331581 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%0D^ka4iDVgpbd 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%0D1kohoXW^QSi1w61HJ{5wh!SF-c}(^S_C#{ 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%0A3(XpKY!>F#GH@a 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 Date: Tue, 24 May 2022 21:43:38 +0200 Subject: [PATCH 515/605] fix: prefer stricter modpack formats during import Flame modpacks use "manifest.json" as their only characteristic for identification. Some modpacks might have other files called "manifest.json", which is why we should prefer modpack formats that have a stricter structure. --- launcher/InstanceImportTask.cpp | 52 +++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 4bad7251..514cbcc5 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -135,18 +135,20 @@ void InstanceImportTask::processZipPack() return; } - QStringList blacklist = {"instance.cfg", "manifest.json"}; - QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); - bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") || QuaZipDir(m_packZip.get()).exists("/bin/version.json"); - QString flameFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json"); - QString modrinthFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "modrinth.index.json"); + QuaZipDir packZipDir(m_packZip.get()); + + // https://docs.modrinth.com/docs/modpacks/format_definition/#storage + bool modrinthFound = packZipDir.exists("/modrinth.index.json"); + bool technicFound = packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json"); QString root; - if(!mmcFound.isNull()) + + // 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 + if(modrinthFound) { - // process as MultiMC instance/pack - qDebug() << "MultiMC:" << mmcFound; - root = mmcFound; - m_modpackType = ModpackType::MultiMC; + // process as Modrinth pack + qDebug() << "Modrinth:" << modrinthFound; + m_modpackType = ModpackType::Modrinth; } else if (technicFound) { @@ -156,19 +158,25 @@ void InstanceImportTask::processZipPack() extractDir.cd(".minecraft"); m_modpackType = ModpackType::Technic; } - else if(!flameFound.isNull()) + else { - // process as Flame pack - qDebug() << "Flame:" << flameFound; - root = flameFound; - m_modpackType = ModpackType::Flame; - } - else if(!modrinthFound.isNull()) - { - // process as Modrinth pack - qDebug() << "Modrinth:" << modrinthFound; - root = modrinthFound; - m_modpackType = ModpackType::Modrinth; + QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); + QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json"); + + if (!mmcRoot.isEmpty()) + { + // process as MultiMC instance/pack + qDebug() << "MultiMC:" << mmcRoot; + root = mmcRoot; + m_modpackType = ModpackType::MultiMC; + } + else if(!flameRoot.isEmpty()) + { + // process as Flame pack + qDebug() << "Flame:" << flameRoot; + root = flameRoot; + m_modpackType = ModpackType::Flame; + } } if(m_modpackType == ModpackType::Unknown) { From 5d3bef32caad17e374559e4718ce73ae2fadbc34 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 27 May 2022 09:15:32 -0300 Subject: [PATCH 516/605] fix: use absolute path when installing icons --- launcher/icons/IconList.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index c269d10a..0ddfae55 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -273,7 +273,7 @@ void IconList::installIcons(const QStringList &iconFiles) QFileInfo fileinfo(file); if (!fileinfo.isReadable() || !fileinfo.isFile()) continue; - QString target = FS::PathCombine(m_dir.dirName(), fileinfo.fileName()); + QString target = FS::PathCombine(getDirectory(), fileinfo.fileName()); QString suffix = fileinfo.suffix(); if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif") @@ -290,7 +290,7 @@ void IconList::installIcon(const QString &file, const QString &name) if(!fileinfo.isReadable() || !fileinfo.isFile()) return; - QString target = FS::PathCombine(m_dir.dirName(), name); + QString target = FS::PathCombine(getDirectory(), name); QFile::copy(file, target); } From 6fb5bb6a5e73d8e967d9bcc142683cdd4ff080ff Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 27 May 2022 14:50:06 +0200 Subject: [PATCH 517/605] fix: fix mnemonics in APIPage --- launcher/ui/pages/global/APIPage.ui | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index cf15065b..5c927391 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -36,13 +36,16 @@ - Pastebin Service + &Pastebin Service - Paste Service Type + Paste Service &Type + + + pasteTypeComboBox @@ -52,7 +55,10 @@ - Base URL + Base &URL + + + baseURLEntry From 283e50e6706074d6a3203e1a4c7b4eede5ffedda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20=C3=87al=C4=B1=C5=9Fkan?= Date: Fri, 27 May 2022 22:23:33 +0300 Subject: [PATCH 518/605] nix: use nixpkgs's quazip --- flake.nix | 5 ++--- packages/nix/polymc/default.nix | 15 +++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/flake.nix b/flake.nix index e59d6be8..b1e81057 100644 --- a/flake.nix +++ b/flake.nix @@ -5,10 +5,9 @@ nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; libnbtplusplus = { url = "github:multimc/libnbtplusplus"; flake = false; }; - quazip = { url = "github:stachenov/quazip"; flake = false; }; }; - outputs = { self, nixpkgs, libnbtplusplus, quazip, ... }: + outputs = { self, nixpkgs, libnbtplusplus, ... }: let # Generate a user-friendly version number. version = builtins.substring 0 8 self.lastModifiedDate; @@ -23,7 +22,7 @@ pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system}); in { - packages = forAllSystems (system: { polymc = pkgs.${system}.libsForQt5.callPackage ./packages/nix/polymc { inherit version self quazip libnbtplusplus; }; }); + packages = forAllSystems (system: { polymc = pkgs.${system}.libsForQt5.callPackage ./packages/nix/polymc { inherit version self libnbtplusplus; }; }); defaultPackage = forAllSystems (system: self.packages.${system}.polymc); apps = forAllSystems (system: { polymc = { type = "app"; program = "${self.defaultPackage.${system}}/bin/polymc"; }; }); diff --git a/packages/nix/polymc/default.nix b/packages/nix/polymc/default.nix index e352209a..d09fe3c7 100644 --- a/packages/nix/polymc/default.nix +++ b/packages/nix/polymc/default.nix @@ -11,6 +11,7 @@ , xorg , libpulseaudio , qtbase +, quazip , libGL , msaClientID ? "" @@ -18,7 +19,6 @@ , self , version , libnbtplusplus -, quazip }: let @@ -43,8 +43,8 @@ mkDerivation rec { src = lib.cleanSource self; - nativeBuildInputs = [ cmake ninja file makeWrapper ]; - buildInputs = [ qtbase jdk zlib ]; + nativeBuildInputs = [ cmake ninja jdk file makeWrapper ]; + buildInputs = [ qtbase quazip zlib ]; dontWrapQtApps = true; @@ -55,12 +55,11 @@ mkDerivation rec { ''; postUnpack = '' - # Copy submodules inputs - rm -rf source/libraries/{libnbtplusplus,quazip} - mkdir source/libraries/{libnbtplusplus,quazip} + # Copy libnbtplusplus + rm -rf source/libraries/libnbtplusplus + mkdir source/libraries/libnbtplusplus cp -a ${libnbtplusplus}/* source/libraries/libnbtplusplus - cp -a ${quazip}/* source/libraries/quazip - chmod a+r+w source/libraries/{libnbtplusplus,quazip}/* + chmod a+r+w source/libraries/libnbtplusplus/* ''; cmakeFlags = [ From bfd9bd43c935a01d7e7b8b078f479970bb81280b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20=C3=87al=C4=B1=C5=9Fkan?= Date: Fri, 27 May 2022 22:25:25 +0300 Subject: [PATCH 519/605] flake.lock: update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Updated input 'flake-compat': 'github:edolstra/flake-compat/64a525ee38886ab9028e6f61790de0832aa3ef03' (2022-03-25) → 'github:edolstra/flake-compat/b4a34015c698c7793d592d66adbab377907a2be8' (2022-04-19) • Updated input 'nixpkgs': 'github:nixos/nixpkgs/30d3d79b7d3607d56546dd2a6b49e156ba0ec634' (2022-03-25) → 'github:nixos/nixpkgs/41cc1d5d9584103be4108c1815c350e07c807036' (2022-05-23) • Removed input 'quazip' --- flake.lock | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/flake.lock b/flake.lock index e3c490fd..ccdd51da 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1648199409, - "narHash": "sha256-JwPKdC2PoVBkG6E+eWw3j6BMR6sL3COpYWfif7RVb8Y=", + "lastModified": 1650374568, + "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=", "owner": "edolstra", "repo": "flake-compat", - "rev": "64a525ee38886ab9028e6f61790de0832aa3ef03", + "rev": "b4a34015c698c7793d592d66adbab377907a2be8", "type": "github" }, "original": { @@ -34,11 +34,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1648219316, - "narHash": "sha256-Ctij+dOi0ZZIfX5eMhgwugfvB+WZSrvVNAyAuANOsnQ=", + "lastModified": 1653326962, + "narHash": "sha256-W8feCYqKTsMre4nAEpv5Kx1PVFC+hao/LwqtB2Wci/8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "30d3d79b7d3607d56546dd2a6b49e156ba0ec634", + "rev": "41cc1d5d9584103be4108c1815c350e07c807036", "type": "github" }, "original": { @@ -48,28 +48,11 @@ "type": "github" } }, - "quazip": { - "flake": false, - "locked": { - "lastModified": 1643049383, - "narHash": "sha256-LcJY6yd6GyeL7X5MP4L94diceM1TYespWByliBsjK98=", - "owner": "stachenov", - "repo": "quazip", - "rev": "09ec1d10c6d627f895109b21728dda000cbfa7d1", - "type": "github" - }, - "original": { - "owner": "stachenov", - "repo": "quazip", - "type": "github" - } - }, "root": { "inputs": { "flake-compat": "flake-compat", "libnbtplusplus": "libnbtplusplus", - "nixpkgs": "nixpkgs", - "quazip": "quazip" + "nixpkgs": "nixpkgs" } } }, From 338156500bfca02427d0bd8c9c6402fc1d5b1122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20=C3=87al=C4=B1=C5=9Fkan?= Date: Fri, 27 May 2022 22:31:25 +0300 Subject: [PATCH 520/605] nix: override msa id via cmake flag --- packages/nix/polymc/default.nix | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/nix/polymc/default.nix b/packages/nix/polymc/default.nix index d09fe3c7..e347db6d 100644 --- a/packages/nix/polymc/default.nix +++ b/packages/nix/polymc/default.nix @@ -48,12 +48,6 @@ mkDerivation rec { dontWrapQtApps = true; - postPatch = lib.optionalString (msaClientID != "") '' - # add client ID - substituteInPlace CMakeLists.txt \ - --replace '17b47edd-c884-4997-926d-9e7f9a6b4647' '${msaClientID}' - ''; - postUnpack = '' # Copy libnbtplusplus rm -rf source/libraries/libnbtplusplus @@ -65,7 +59,7 @@ mkDerivation rec { cmakeFlags = [ "-GNinja" "-DLauncher_PORTABLE=OFF" - ]; + ] ++ lib.optionals (msaClientID != "") [ "-DLauncher_MSA_CLIENT_ID=${msaClientID}" ]; postInstall = '' # xorg.xrandr needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 From 0ffe0b6894ef3621ad0b345b5996509c4c95d919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20=C3=87al=C4=B1=C5=9Fkan?= Date: Fri, 27 May 2022 22:34:44 +0300 Subject: [PATCH 521/605] nix: move files to nix/ --- flake.nix | 2 +- {packages/nix => nix}/NIX.md | 0 {packages/nix/polymc => nix}/default.nix | 0 {packages/nix => nix}/flake-compat.nix | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename {packages/nix => nix}/NIX.md (100%) rename {packages/nix/polymc => nix}/default.nix (100%) rename {packages/nix => nix}/flake-compat.nix (100%) diff --git a/flake.nix b/flake.nix index b1e81057..afc85336 100644 --- a/flake.nix +++ b/flake.nix @@ -22,7 +22,7 @@ pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system}); in { - packages = forAllSystems (system: { polymc = pkgs.${system}.libsForQt5.callPackage ./packages/nix/polymc { inherit version self libnbtplusplus; }; }); + packages = forAllSystems (system: { polymc = pkgs.${system}.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus; }; }); defaultPackage = forAllSystems (system: self.packages.${system}.polymc); apps = forAllSystems (system: { polymc = { type = "app"; program = "${self.defaultPackage.${system}}/bin/polymc"; }; }); diff --git a/packages/nix/NIX.md b/nix/NIX.md similarity index 100% rename from packages/nix/NIX.md rename to nix/NIX.md diff --git a/packages/nix/polymc/default.nix b/nix/default.nix similarity index 100% rename from packages/nix/polymc/default.nix rename to nix/default.nix diff --git a/packages/nix/flake-compat.nix b/nix/flake-compat.nix similarity index 100% rename from packages/nix/flake-compat.nix rename to nix/flake-compat.nix From 48e20cb5f714fbee83889d55505eb99c3f444cda Mon Sep 17 00:00:00 2001 From: Jeremy Lorelli Date: Fri, 27 May 2022 16:41:57 -0700 Subject: [PATCH 522/605] Fix crash when aborting instance import Also turned a loop var into a reference to avoid copies on each iteration --- launcher/InstanceImportTask.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 4bad7251..56081ed1 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -72,7 +72,8 @@ InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent) bool InstanceImportTask::abort() { - m_filesNetJob->abort(); + if (m_filesNetJob) + m_filesNetJob->abort(); m_extractFuture.cancel(); return false; @@ -386,7 +387,7 @@ void InstanceImportTask::processFlame() { auto results = m_modIdResolver->getResults(); m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); - for(auto result: results.files) + for(const auto& result: results.files) { QString filename = result.fileName; if(!result.required) From 0ea2135aa54dbfe582e3d91cefbec1d22ffedabc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20=C3=87al=C4=B1=C5=9Fkan?= Date: Fri, 27 May 2022 22:42:23 +0300 Subject: [PATCH 523/605] nix: initial support for qt6 --- flake.nix | 6 +++++- nix/default.nix | 17 ++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/flake.nix b/flake.nix index afc85336..f2247bed 100644 --- a/flake.nix +++ b/flake.nix @@ -22,7 +22,11 @@ pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system}); in { - packages = forAllSystems (system: { polymc = pkgs.${system}.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus; }; }); + packages = forAllSystems (system: { + polymc = pkgs.${system}.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus; }; + polymc-qt6 = pkgs.${system}.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus; }; + }); + defaultPackage = forAllSystems (system: self.packages.${system}.polymc); apps = forAllSystems (system: { polymc = { type = "app"; program = "${self.defaultPackage.${system}}/bin/polymc"; }; }); diff --git a/nix/default.nix b/nix/default.nix index e347db6d..cce40e63 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,5 +1,5 @@ -{ lib -, mkDerivation +{ stdenv +, lib , fetchFromGitHub , cmake , ninja @@ -7,11 +7,10 @@ , jdk , zlib , file -, makeWrapper +, wrapQtAppsHook , xorg , libpulseaudio , qtbase -, quazip , libGL , msaClientID ? "" @@ -37,13 +36,13 @@ let gameLibraryPath = libpath + ":/run/opengl-driver/lib"; in -mkDerivation rec { +stdenv.mkDerivation rec { pname = "polymc"; inherit version; src = lib.cleanSource self; - nativeBuildInputs = [ cmake ninja jdk file makeWrapper ]; + nativeBuildInputs = [ cmake ninja jdk file wrapQtAppsHook ]; buildInputs = [ qtbase quazip zlib ]; dontWrapQtApps = true; @@ -58,13 +57,13 @@ mkDerivation rec { cmakeFlags = [ "-GNinja" - "-DLauncher_PORTABLE=OFF" + "-DENABLE_LTO=on" + "-DLauncher_QT_VERSION_MAJOR=${lib.versions.major qtbase.version}" ] ++ lib.optionals (msaClientID != "") [ "-DLauncher_MSA_CLIENT_ID=${msaClientID}" ]; postInstall = '' # xorg.xrandr needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 - wrapProgram $out/bin/polymc \ - "''${qtWrapperArgs[@]}" \ + wrapQtApp $out/bin/polymc \ --set GAME_LIBRARY_PATH ${gameLibraryPath} \ --prefix POLYMC_JAVA_PATHS : ${jdk}/lib/openjdk/bin/java:${jdk8}/lib/openjdk/bin/java \ --prefix PATH : ${lib.makeBinPath [ xorg.xrandr ]} From 123d6c72e4308a0194d57f5a910d063bd84941e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20=C3=87al=C4=B1=C5=9Fkan?= Date: Sat, 28 May 2022 11:11:31 +0300 Subject: [PATCH 524/605] nix: fix nix-build --- default.nix | 2 +- nix/flake-compat.nix | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/default.nix b/default.nix index 5abfc1bd..146942d5 100644 --- a/default.nix +++ b/default.nix @@ -1 +1 @@ -(import packages/nix/flake-compat.nix).defaultNix +(import nix/flake-compat.nix).defaultNix diff --git a/nix/flake-compat.nix b/nix/flake-compat.nix index bb7ee13e..8b6cb99c 100644 --- a/nix/flake-compat.nix +++ b/nix/flake-compat.nix @@ -1,9 +1,9 @@ let - lock = builtins.fromJSON (builtins.readFile ../../flake.lock); + lock = builtins.fromJSON (builtins.readFile ../flake.lock); inherit (lock.nodes.flake-compat.locked) rev narHash; flake-compat = fetchTarball { url = "https://github.com/edolstra/flake-compat/archive/${rev}.tar.gz"; sha256 = narHash; }; in -import flake-compat { src = ../..; } +import flake-compat { src = ../.; } From ab3e2562db52d66f690f08621e220766b0953af7 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 28 May 2022 12:07:01 +0200 Subject: [PATCH 525/605] fix: clarify terms and conditions for API keys --- CMakeLists.txt | 23 ++++++++++++++++------- README.md | 7 +++++-- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fcc2512d..5c893ceb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,13 +91,6 @@ set(Launcher_META_URL "https://meta.polymc.org/v1/" CACHE STRING "URL to fetch L # Imgur API Client ID set(Launcher_IMGUR_CLIENT_ID "5b97b0713fba4a3" CACHE STRING "Client ID you can get from Imgur when you register an application") -# MSA Client ID -set(Launcher_MSA_CLIENT_ID "549033b2-1532-4d4e-ae77-1bbaa46f9d74" CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application") - -# CurseForge API Key -# CHANGE THIS IF YOU FORK THIS PROJECT! -set(Launcher_CURSEFORGE_API_KEY "$2a$10$1Oqr2MX3O4n/ilhFGc597u8tfI3L2Hyr9/rtWDAMRjghSQV2QUuxq" CACHE STRING "CurseForge API Key") - # Bug tracker URL set(Launcher_BUG_TRACKER_URL "https://github.com/PolyMC/PolyMC/issues" CACHE STRING "URL for the bug tracker.") @@ -119,6 +112,22 @@ set(Launcher_SUBREDDIT_URL "https://www.reddit.com/r/PolyMCLauncher/" CACHE STRI set(Launcher_FORCE_BUNDLED_LIBS OFF CACHE BOOL "Prevent using system libraries, if they are available as submodules") set(Launcher_QT_VERSION_MAJOR "5" CACHE STRING "Major Qt version to build against") +# API Keys +# NOTE: These API keys are here for convenience. If you rebrand this software or intend to break the terms of service +# of these platforms, please change these API keys beforehand. +# Be aware that if you were to use these API keys for malicious purposes they might get revoked, which might cause +# breakage to thousands of users. +# If you don't plan to use these features of this software, you can just remove these values. + +# 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 "549033b2-1532-4d4e-ae77-1bbaa46f9d74" 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 +# NOTE: CurseForge requires you to change this if you make any kind of derivative work. +set(Launcher_CURSEFORGE_API_KEY "$2a$10$1Oqr2MX3O4n/ilhFGc597u8tfI3L2Hyr9/rtWDAMRjghSQV2QUuxq" CACHE STRING "CurseForge API Key") + #### Check the current Git commit and branch include(GetGitRevisionDescription) diff --git a/README.md b/README.md index c493293d..a08d5dc0 100644 --- a/README.md +++ b/README.md @@ -82,8 +82,11 @@ To modify download information or change packaging information send a pull reque Do whatever you want, we don't care. Just follow the license. If you have any questions about this feel free to ask in an issue. +Be aware that if you build this software without removing the provided API keys in [CMakeLists.txt](CMakeLists.txt) you are accepting the following terms and conditions: + - [Microsoft Identity Platform Terms of Use](https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use) + - [CurseForge 3rd Party API Terms and Conditions](https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions) +If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file. + All launcher code is available under the GPL-3.0-only license. -[Source for the website](https://github.com/PolyMC/polymc.github.io) is hosted under the AGPL-3.0-or-later License. - The logo and related assets are under the CC BY-SA 4.0 license. From 80627b4f8914c821f125893dc3c04380530d6e0b Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 28 May 2022 15:42:54 +0200 Subject: [PATCH 526/605] chore: bump version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fcc2512d..320984df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,7 +74,7 @@ set(Launcher_HELP_URL "https://polymc.org/wiki/help-pages/%1" CACHE STRING "URL ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 1) set(Launcher_VERSION_MINOR 3) -set(Launcher_VERSION_HOTFIX 0) +set(Launcher_VERSION_HOTFIX 1) # Build number set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") From 699ad316f0d90580fa13d570d6c25aff903a470d Mon Sep 17 00:00:00 2001 From: timoreo22 Date: Sat, 28 May 2022 21:53:12 +0200 Subject: [PATCH 527/605] Rework curseforge download (#611) * Use the bulk endpoint on mod resolution for faster download * Search on modrinth for api blocked mods * Display a dialog for manually downloading blocked mods --- launcher/CMakeLists.txt | 5 + launcher/InstanceImportTask.cpp | 191 ++++++++++++----- launcher/InstanceImportTask.h | 6 + .../modplatform/flame/FileResolvingTask.cpp | 126 ++++++++--- .../modplatform/flame/FileResolvingTask.h | 8 +- launcher/modplatform/flame/PackManifest.cpp | 35 +-- launcher/modplatform/flame/PackManifest.h | 10 +- launcher/net/Upload.cpp | 199 ++++++++++++++++++ launcher/net/Upload.h | 31 +++ launcher/ui/dialogs/ScrollMessageBox.cpp | 15 ++ launcher/ui/dialogs/ScrollMessageBox.h | 20 ++ launcher/ui/dialogs/ScrollMessageBox.ui | 84 ++++++++ launcher/ui/pages/modplatform/ImportPage.cpp | 4 +- .../ui/pages/modplatform/flame/FlamePage.cpp | 2 +- 14 files changed, 633 insertions(+), 103 deletions(-) create mode 100644 launcher/net/Upload.cpp create mode 100644 launcher/net/Upload.h create mode 100644 launcher/ui/dialogs/ScrollMessageBox.cpp create mode 100644 launcher/ui/dialogs/ScrollMessageBox.h create mode 100644 launcher/ui/dialogs/ScrollMessageBox.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 15534c71..b3af12a6 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -128,6 +128,8 @@ set(NET_SOURCES net/PasteUpload.h net/Sink.h net/Validator.h + net/Upload.cpp + net/Upload.h ) # Game launch logic @@ -837,6 +839,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/SkinUploadDialog.h ui/dialogs/ModDownloadDialog.cpp ui/dialogs/ModDownloadDialog.h + ui/dialogs/ScrollMessageBox.cpp + ui/dialogs/ScrollMessageBox.h # GUI - widgets ui/widgets/Common.cpp @@ -940,6 +944,7 @@ qt5_wrap_ui(LAUNCHER_UI ui/dialogs/LoginDialog.ui ui/dialogs/EditAccountDialog.ui ui/dialogs/ReviewMessageBox.ui + ui/dialogs/ScrollMessageBox.ui ) qt5_add_resources(LAUNCHER_RESOURCES diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 4acde16d..68a497cc 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -60,9 +60,9 @@ #include "net/ChecksumValidator.h" #include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ScrollMessageBox.h" #include -#include InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent) { @@ -394,61 +394,136 @@ void InstanceImportTask::processFlame() connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]() { auto results = m_modIdResolver->getResults(); - m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); - for(const auto& result: results.files) - { - QString filename = result.fileName; - if(!result.required) - { - filename += ".disabled"; - } - - auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename); - auto path = FS::PathCombine(m_stagingPath , relpath); - - switch(result.type) - { - case Flame::File::Type::Folder: - { - logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); - // fall-through intentional, we treat these as plain old mods and dump them wherever. - } - case Flame::File::Type::SingleFile: - case Flame::File::Type::Mod: - { - qDebug() << "Will download" << result.url << "to" << path; - auto dl = Net::Download::makeFile(result.url, path); - m_filesNetJob->addNetAction(dl); - break; - } - case Flame::File::Type::Modpack: - logWarning(tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(relpath)); - break; - case Flame::File::Type::Cmod2: - case Flame::File::Type::Ctoc: - case Flame::File::Type::Unknown: - logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath)); - break; + //first check for blocked mods + QString text; + auto anyBlocked = false; + for(const auto& result: results.files.values()) { + if (!result.resolved || result.url.isEmpty()) { + text += QString("%1: %2
").arg(result.fileName, result.websiteUrl); + anyBlocked = true; } } - m_modIdResolver.reset(); - connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() - { - m_filesNetJob.reset(); - emitSucceeded(); + if(anyBlocked) { + qWarning() << "Blocked mods found, displaying mod list"; + + auto message_dialog = new ScrollMessageBox(m_parent, + tr("Blocked mods found"), + tr("The following mods were blocked on third party launchers.
" + "You will need to manually download them and add them to the modpack"), + text); + message_dialog->setModal(true); + message_dialog->show(); + connect(message_dialog, &QDialog::rejected, [&]() { + m_modIdResolver.reset(); + emitFailed("Canceled"); + }); + connect(message_dialog, &QDialog::accepted, [&]() { + m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); + for (const auto &result: m_modIdResolver->getResults().files) { + QString filename = result.fileName; + if (!result.required) { + filename += ".disabled"; + } + + auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename); + auto path = FS::PathCombine(m_stagingPath, relpath); + + switch (result.type) { + case Flame::File::Type::Folder: { + logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); + // fall-through intentional, we treat these as plain old mods and dump them wherever. + } + case Flame::File::Type::SingleFile: + case Flame::File::Type::Mod: { + if (!result.url.isEmpty()) { + qDebug() << "Will download" << result.url << "to" << path; + auto dl = Net::Download::makeFile(result.url, path); + m_filesNetJob->addNetAction(dl); + } + break; + } + case Flame::File::Type::Modpack: + logWarning( + tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg( + relpath)); + break; + case Flame::File::Type::Cmod2: + case Flame::File::Type::Ctoc: + case Flame::File::Type::Unknown: + logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath)); + break; + } + } + m_modIdResolver.reset(); + connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() { + m_filesNetJob.reset(); + emitSucceeded(); + } + ); + connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) { + m_filesNetJob.reset(); + emitFailed(reason); + }); + connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + setProgress(current, total); + }); + setStatus(tr("Downloading mods...")); + m_filesNetJob->start(); + }); + }else{ + //TODO extract to function ? + m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); + for (const auto &result: m_modIdResolver->getResults().files) { + QString filename = result.fileName; + if (!result.required) { + filename += ".disabled"; + } + + auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename); + auto path = FS::PathCombine(m_stagingPath, relpath); + + switch (result.type) { + case Flame::File::Type::Folder: { + logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); + // fall-through intentional, we treat these as plain old mods and dump them wherever. + } + case Flame::File::Type::SingleFile: + case Flame::File::Type::Mod: { + if (!result.url.isEmpty()) { + qDebug() << "Will download" << result.url << "to" << path; + auto dl = Net::Download::makeFile(result.url, path); + m_filesNetJob->addNetAction(dl); + } + break; + } + case Flame::File::Type::Modpack: + logWarning( + tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg( + relpath)); + break; + case Flame::File::Type::Cmod2: + case Flame::File::Type::Ctoc: + case Flame::File::Type::Unknown: + logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath)); + break; + } + } + m_modIdResolver.reset(); + connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() { + m_filesNetJob.reset(); + emitSucceeded(); + } + ); + connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) { + m_filesNetJob.reset(); + emitFailed(reason); + }); + connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + setProgress(current, total); + }); + setStatus(tr("Downloading mods...")); + m_filesNetJob->start(); } - ); - connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) - { - m_filesNetJob.reset(); - emitFailed(reason); - }); - connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) - { - setProgress(current, total); - }); - setStatus(tr("Downloading mods...")); - m_filesNetJob->start(); } ); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason) @@ -524,11 +599,11 @@ void InstanceImportTask::processModrinth() auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); bool had_optional = false; - for (auto& obj : jsonFiles) { + for (auto& modInfo : jsonFiles) { Modrinth::File file; - file.path = Json::requireString(obj, "path"); + file.path = Json::requireString(modInfo, "path"); - auto env = Json::ensureObject(obj, "env"); + auto env = Json::ensureObject(modInfo, "env"); QString support = Json::ensureString(env, "client", "unsupported"); if (support == "unsupported") { continue; @@ -546,7 +621,7 @@ void InstanceImportTask::processModrinth() file.path += ".disabled"; } - QJsonObject hashes = Json::requireObject(obj, "hashes"); + QJsonObject hashes = Json::requireObject(modInfo, "hashes"); QString hash; QCryptographicHash::Algorithm hashAlgorithm; hash = Json::ensureString(hashes, "sha1"); @@ -566,7 +641,7 @@ void InstanceImportTask::processModrinth() file.hashAlgorithm = hashAlgorithm; // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode // (as Modrinth seems to incorrectly handle spaces) - file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); + file.download = Json::requireString(Json::ensureArray(modInfo, "downloads").first(), "Download URL for " + file.path); if (!file.download.isValid() || !Modrinth::validateDownloadUrl(file.download)) { throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); } diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 5e4d3235..b67d48f3 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -42,6 +42,7 @@ #include #include "settings/SettingsObject.h" #include "QObjectPtr.h" +#include "modplatform/flame/PackManifest.h" #include @@ -59,6 +60,10 @@ public: bool canAbort() const override { return true; } bool abort() override; + const QVector &getBlockedFiles() const + { + return m_blockedMods; + } protected: //! Entry point for tasks. @@ -87,6 +92,7 @@ private: /* data */ std::unique_ptr m_packZip; QFuture> m_extractFuture; QFutureWatcher> m_extractFutureWatcher; + QVector m_blockedMods; enum class ModpackType{ Unknown, MultiMC, diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 95924a68..a790ab9c 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -1,7 +1,9 @@ #include "FileResolvingTask.h" -#include "Json.h" -Flame::FileResolvingTask::FileResolvingTask(shared_qobject_ptr network, Flame::Manifest& toProcess) +#include "Json.h" +#include "net/Upload.h" + +Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr& network, Flame::Manifest& toProcess) : m_network(network), m_toProcess(toProcess) {} @@ -10,40 +12,116 @@ void Flame::FileResolvingTask::executeTask() setStatus(tr("Resolving mod IDs...")); setProgress(0, m_toProcess.files.size()); m_dljob = new NetJob("Mod id resolver", m_network); - results.resize(m_toProcess.files.size()); - int index = 0; - for (auto& file : m_toProcess.files) { - auto projectIdStr = QString::number(file.projectId); - auto fileIdStr = QString::number(file.fileId); - QString metaurl = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(projectIdStr, fileIdStr); - auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]); - m_dljob->addNetAction(dl); - index++; - } + result.reset(new QByteArray()); + //build json data to send + QJsonObject object; + + object["fileIds"] = QJsonArray::fromVariantList(std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) { + l.push_back(s.fileId); + return l; + })); + QByteArray data = Json::toText(object); + auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result.get(), data); + m_dljob->addNetAction(dl); connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished); m_dljob->start(); } void Flame::FileResolvingTask::netJobFinished() { - bool failed = false; int index = 0; - for (auto& bytes : results) { - auto& out = m_toProcess.files[index]; + // job to check modrinth for blocked projects + auto job = new NetJob("Modrinth check", m_network); + blockedProjects = QMap(); + auto doc = Json::requireDocument(*result); + auto array = Json::requireArray(doc.object()["data"]); + for (QJsonValueRef file : array) { + auto fileid = Json::requireInteger(Json::requireObject(file)["id"]); + auto& out = m_toProcess.files[fileid]; try { - failed &= (!out.parseFromBytes(bytes)); + out.parseFromObject(Json::requireObject(file)); } catch (const JSONValidationError& e) { - qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:"; - qCritical() << e.cause(); - qCritical() << "JSON:"; - qCritical() << bytes; - failed = true; + qDebug() << "Blocked mod on curseforge" << out.fileName; + auto hash = out.hash; + if(!hash.isEmpty()) { + auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash); + auto output = new QByteArray(); + auto dl = Net::Download::makeByteArray(QUrl(url), output); + QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() { + out.resolved = true; + }); + + job->addNetAction(dl); + blockedProjects.insert(&out, output); + } } index++; } - if (!failed) { - emitSucceeded(); + connect(job, &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished); + + job->start(); +} + +void Flame::FileResolvingTask::modrinthCheckFinished() { + qDebug() << "Finished with blocked mods : " << blockedProjects.size(); + + for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) { + auto &out = *it; + auto bytes = blockedProjects[out]; + if (!out->resolved) { + delete bytes; + continue; + } + QJsonDocument doc = QJsonDocument::fromJson(*bytes); + auto obj = doc.object(); + auto array = Json::requireArray(obj,"files"); + for (auto file: array) { + auto fileObj = Json::requireObject(file); + auto primary = Json::requireBoolean(fileObj,"primary"); + if (primary) { + out->url = Json::requireUrl(fileObj,"url"); + qDebug() << "Found alternative on modrinth " << out->fileName; + break; + } + } + delete bytes; + } + //copy to an output list and filter out projects found on modrinth + auto block = new QList(); + auto it = blockedProjects.keys(); + std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File *f) { + return !f->resolved; + }); + //Display not found mods early + if (!block->empty()) { + //blocked mods found, we need the slug for displaying.... we need another job :D ! + auto slugJob = new NetJob("Slug Job", m_network); + auto slugs = QVector(block->size()); + auto index = 0; + for (auto fileInfo: *block) { + auto projectId = fileInfo->projectId; + slugs[index] = QByteArray(); + auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId); + auto dl = Net::Download::makeByteArray(url, &slugs[index]); + slugJob->addNetAction(dl); + index++; + } + connect(slugJob, &NetJob::succeeded, this, [slugs, this, slugJob, block]() { + slugJob->deleteLater(); + auto index = 0; + for (const auto &slugResult: slugs) { + auto json = QJsonDocument::fromJson(slugResult); + auto base = Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json),"data"),"links"), + "websiteUrl"); + auto mod = block->at(index); + auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId)); + mod->websiteUrl = link; + index++; + } + emitSucceeded(); + }); + slugJob->start(); } else { - emitFailed(tr("Some mod ID resolving tasks failed.")); + emitSucceeded(); } } diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h index 5e5adcd7..87981f0a 100644 --- a/launcher/modplatform/flame/FileResolvingTask.h +++ b/launcher/modplatform/flame/FileResolvingTask.h @@ -10,7 +10,7 @@ class FileResolvingTask : public Task { Q_OBJECT public: - explicit FileResolvingTask(shared_qobject_ptr network, Flame::Manifest &toProcess); + explicit FileResolvingTask(const shared_qobject_ptr& network, Flame::Manifest &toProcess); virtual ~FileResolvingTask() {}; const Flame::Manifest &getResults() const @@ -27,7 +27,11 @@ protected slots: private: /* data */ shared_qobject_ptr m_network; Flame::Manifest m_toProcess; - QVector results; + std::shared_ptr result; NetJob::Ptr m_dljob; + + void modrinthCheckFinished(); + + QMap blockedProjects; }; } diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index e4f90c1a..12a4b990 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -41,7 +41,7 @@ static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest) auto obj = Json::requireObject(item); Flame::File file; loadFileV1(file, obj); - m.files.append(file); + m.files.insert(file.fileId,file); } m.overrides = Json::ensureString(manifest, "overrides", "overrides"); } @@ -61,21 +61,9 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath) loadManifestV1(m, obj); } -bool Flame::File::parseFromBytes(const QByteArray& bytes) +bool Flame::File::parseFromObject(const QJsonObject& obj) { - auto doc = Json::requireDocument(bytes); - if (!doc.isObject()) { - throw JSONValidationError(QString("data is not an object? that's not supposed to happen")); - } - auto obj = Json::ensureObject(doc.object(), "data"); - fileName = Json::requireString(obj, "fileName"); - - QString rawUrl = Json::requireString(obj, "downloadUrl"); - url = QUrl(rawUrl, QUrl::TolerantMode); - if (!url.isValid()) { - throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); - } // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience // It is also optional type = File::Type::SingleFile; @@ -87,6 +75,25 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes) // this is probably a mod, dunno what else could modpacks download targetFolder = "mods"; } + // get the hash + hash = QString(); + auto hashes = Json::ensureArray(obj, "hashes"); + for(QJsonValueRef item : hashes) { + auto hobj = Json::requireObject(item); + auto algo = Json::requireInteger(hobj, "algo"); + auto value = Json::requireString(hobj, "value"); + if (algo == 1) { + hash = value; + } + } + + + // may throw, if the project is blocked + QString rawUrl = Json::ensureString(obj, "downloadUrl"); + url = QUrl(rawUrl, QUrl::TolerantMode); + if (!url.isValid()) { + throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); + } resolved = true; return true; diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h index 02f39f0e..26a48d1c 100644 --- a/launcher/modplatform/flame/PackManifest.h +++ b/launcher/modplatform/flame/PackManifest.h @@ -2,19 +2,24 @@ #include #include +#include #include +#include namespace Flame { struct File { // NOTE: throws JSONValidationError - bool parseFromBytes(const QByteArray &bytes); + bool parseFromObject(const QJsonObject& object); int projectId = 0; int fileId = 0; // NOTE: the opposite to 'optional'. This is at the time of writing unused. bool required = true; + QString hash; + // NOTE: only set on blocked files ! Empty otherwise. + QString websiteUrl; // our bool resolved = false; @@ -54,7 +59,8 @@ struct Manifest QString name; QString version; QString author; - QVector files; + //File id -> File + QMap files; QString overrides; }; diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp new file mode 100644 index 00000000..bbd27390 --- /dev/null +++ b/launcher/net/Upload.cpp @@ -0,0 +1,199 @@ +// +// Created by timoreo on 20/05/22. +// + +#include "Upload.h" + +#include +#include "ByteArraySink.h" +#include "BuildConfig.h" +#include "Application.h" + +namespace Net { + + void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { + setProgress(bytesReceived, bytesTotal); + } + + void Upload::downloadError(QNetworkReply::NetworkError error) { + if (error == QNetworkReply::OperationCanceledError) { + qCritical() << "Aborted " << m_url.toString(); + m_state = State::AbortedByUser; + } else { + // error happened during download. + qCritical() << "Failed " << m_url.toString() << " with reason " << error; + m_state = State::Failed; + } + } + + void Upload::sslErrors(const QList &errors) { + int i = 1; + for (const auto& error : errors) { + qCritical() << "Upload" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + qCritical() << "Certificate in question:\n" << cert.toText(); + i++; + } + } + + bool Upload::handleRedirect() + { + QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); + if (!redirect.isValid()) { + if (!m_reply->hasRawHeader("Location")) { + // no redirect -> it's fine to continue + return false; + } + // there is a Location header, but it's not correct. we need to apply some workarounds... + QByteArray redirectBA = m_reply->rawHeader("Location"); + if (redirectBA.size() == 0) { + // empty, yet present redirect header? WTF? + return false; + } + QString redirectStr = QString::fromUtf8(redirectBA); + + if (redirectStr.startsWith("//")) { + /* + * IF the URL begins with //, we need to insert the URL scheme. + * See: https://bugreports.qt.io/browse/QTBUG-41061 + * See: http://tools.ietf.org/html/rfc3986#section-4.2 + */ + redirectStr = m_reply->url().scheme() + ":" + redirectStr; + } else if (redirectStr.startsWith("/")) { + /* + * IF the URL begins with /, we need to process it as a relative URL + */ + auto url = m_reply->url(); + url.setPath(redirectStr, QUrl::TolerantMode); + redirectStr = url.toString(); + } + + /* + * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues. + * FIXME: report Qt bug for this + */ + redirect = QUrl(redirectStr, QUrl::TolerantMode); + if (!redirect.isValid()) { + qWarning() << "Failed to parse redirect URL:" << redirectStr; + downloadError(QNetworkReply::ProtocolFailure); + return false; + } + qDebug() << "Fixed location header:" << redirect; + } else { + qDebug() << "Location header:" << redirect; + } + + m_url = QUrl(redirect.toString()); + qDebug() << "Following redirect to " << m_url.toString(); + startAction(m_network); + return true; + } + + void Upload::downloadFinished() { + // handle HTTP redirection first + // very unlikely for post requests, still can happen + if (handleRedirect()) { + qDebug() << "Upload redirected:" << m_url.toString(); + return; + } + + // if the download failed before this point ... + if (m_state == State::Succeeded) { + qDebug() << "Upload failed but we are allowed to proceed:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit succeeded(); + return; + } else if (m_state == State::Failed) { + qDebug() << "Upload failed in previous step:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit failed(""); + return; + } else if (m_state == State::AbortedByUser) { + qDebug() << "Upload aborted in previous step:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit aborted(); + return; + } + + // make sure we got all the remaining data, if any + auto data = m_reply->readAll(); + if (data.size()) { + qDebug() << "Writing extra" << data.size() << "bytes"; + m_state = m_sink->write(data); + } + + // otherwise, finalize the whole graph + m_state = m_sink->finalize(*m_reply.get()); + if (m_state != State::Succeeded) { + qDebug() << "Upload failed to finalize:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit failed(""); + return; + } + m_reply.reset(); + qDebug() << "Upload succeeded:" << m_url.toString(); + emit succeeded(); + } + + void Upload::downloadReadyRead() { + if (m_state == State::Running) { + auto data = m_reply->readAll(); + m_state = m_sink->write(data); + } + } + + void Upload::executeTask() { + setStatus(tr("Uploading %1").arg(m_url.toString())); + + if (m_state == State::AbortedByUser) { + qWarning() << "Attempt to start an aborted Upload:" << m_url.toString(); + emit aborted(); + return; + } + QNetworkRequest request(m_url); + m_state = m_sink->init(request); + switch (m_state) { + case State::Succeeded: + emitSucceeded(); + qDebug() << "Upload cache hit " << m_url.toString(); + return; + case State::Running: + qDebug() << "Uploading " << m_url.toString(); + break; + case State::Inactive: + case State::Failed: + emitFailed(""); + return; + case State::AbortedByUser: + emitAborted(); + return; + } + + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); + if (request.url().host().contains("api.curseforge.com")) { + request.setRawHeader("x-api-key", APPLICATION->getCurseKey().toUtf8()); + } + //TODO other types of post requests ? + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + QNetworkReply* rep = m_network->post(request, m_post_data); + + m_reply.reset(rep); + connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64))); + connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, &QNetworkReply::sslErrors, this, &Upload::sslErrors); + connect(rep, &QNetworkReply::readyRead, this, &Upload::downloadReadyRead); + } + + Upload::Ptr Upload::makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data) { + auto* up = new Upload(); + up->m_url = std::move(url); + up->m_sink.reset(new ByteArraySink(output)); + up->m_post_data = std::move(m_post_data); + return up; + } +} // Net diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h new file mode 100644 index 00000000..ee784c6e --- /dev/null +++ b/launcher/net/Upload.h @@ -0,0 +1,31 @@ +#pragma once + +#include "NetAction.h" +#include "Sink.h" + +namespace Net { + + class Upload : public NetAction { + Q_OBJECT + + public: + static Upload::Ptr makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data); + + protected slots: + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; + void downloadError(QNetworkReply::NetworkError error) override; + void sslErrors(const QList & errors); + void downloadFinished() override; + void downloadReadyRead() override; + + public slots: + void executeTask() override; + private: + std::unique_ptr m_sink; + QByteArray m_post_data; + + bool handleRedirect(); + }; + +} // Net + diff --git a/launcher/ui/dialogs/ScrollMessageBox.cpp b/launcher/ui/dialogs/ScrollMessageBox.cpp new file mode 100644 index 00000000..afdc4bae --- /dev/null +++ b/launcher/ui/dialogs/ScrollMessageBox.cpp @@ -0,0 +1,15 @@ +#include "ScrollMessageBox.h" +#include "ui_ScrollMessageBox.h" + + +ScrollMessageBox::ScrollMessageBox(QWidget *parent, const QString &title, const QString &text, const QString &body) : + QDialog(parent), ui(new Ui::ScrollMessageBox) { + ui->setupUi(this); + this->setWindowTitle(title); + ui->label->setText(text); + ui->textBrowser->setText(body); +} + +ScrollMessageBox::~ScrollMessageBox() { + delete ui; +} diff --git a/launcher/ui/dialogs/ScrollMessageBox.h b/launcher/ui/dialogs/ScrollMessageBox.h new file mode 100644 index 00000000..84aa253a --- /dev/null +++ b/launcher/ui/dialogs/ScrollMessageBox.h @@ -0,0 +1,20 @@ +#pragma once + +#include + + +QT_BEGIN_NAMESPACE +namespace Ui { class ScrollMessageBox; } +QT_END_NAMESPACE + +class ScrollMessageBox : public QDialog { +Q_OBJECT + +public: + ScrollMessageBox(QWidget *parent, const QString &title, const QString &text, const QString &body); + + ~ScrollMessageBox() override; + +private: + Ui::ScrollMessageBox *ui; +}; diff --git a/launcher/ui/dialogs/ScrollMessageBox.ui b/launcher/ui/dialogs/ScrollMessageBox.ui new file mode 100644 index 00000000..885fbfd2 --- /dev/null +++ b/launcher/ui/dialogs/ScrollMessageBox.ui @@ -0,0 +1,84 @@ + + + ScrollMessageBox + + + + 0 + 0 + 400 + 455 + + + + ScrollMessageBox + + + + + + + + + Qt::RichText + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + true + + + true + + + + + + + + + buttonBox + accepted() + ScrollMessageBox + accept() + + + 199 + 425 + + + 199 + 227 + + + + + buttonBox + rejected() + ScrollMessageBox + reject() + + + 199 + 425 + + + 199 + 227 + + + + + diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index c7bc13d8..b3ed1b73 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -117,7 +117,7 @@ void ImportPage::updateState() if(fi.exists() && (zip || fi.suffix() == "mrpack")) { QFileInfo fi(url.fileName()); - dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); + dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this)); dialog->setSuggestedIcon("default"); } } @@ -130,7 +130,7 @@ void ImportPage::updateState() } // hook, line and sinker. QFileInfo fi(url.fileName()); - dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); + dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this)); dialog->setSuggestedIcon("default"); } } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index ec774621..7e90af47 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -201,7 +201,7 @@ void FlamePage::suggestCurrent() return; } - dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion)); + dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion,this)); QString editedLogoName; editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0); listModel->getLogo(current.logoName, current.logoUrl, From f4604bbf797673b089367ec6af42723084b17181 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 28 May 2022 09:19:53 -0300 Subject: [PATCH 528/605] change: update whitelisted hosts in Modrinth modpacks --- launcher/InstanceImportTask.cpp | 11 ++++++++--- .../modplatform/modrinth/ModrinthPackManifest.cpp | 4 ---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 68a497cc..e3f54aeb 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -641,10 +641,15 @@ void InstanceImportTask::processModrinth() file.hashAlgorithm = hashAlgorithm; // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode // (as Modrinth seems to incorrectly handle spaces) + file.download = Json::requireString(Json::ensureArray(modInfo, "downloads").first(), "Download URL for " + file.path); - if (!file.download.isValid() || !Modrinth::validateDownloadUrl(file.download)) { - throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); - } + + if(!file.download.isValid()) + throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); + else if(!Modrinth::validateDownloadUrl(file.download)) + throw JSONValidationError( + tr("Download URL for %1 is from a non-whitelisted by Modrinth domain: %2").arg(file.path, file.download.host())); + files.push_back(file); } diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index f1ad39ce..b1c4fbcd 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -98,10 +98,6 @@ auto validateDownloadUrl(QUrl url) -> bool auto domain = url.host(); if(domain == "cdn.modrinth.com") return true; - if(domain == "edge.forgecdn.net") - return true; - if(domain == "media.forgecdn.net") - return true; if(domain == "github.com") return true; if(domain == "raw.githubusercontent.com") From 1698554024d8fb7646a7a725e354a960ee19b568 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 28 May 2022 14:12:00 -0300 Subject: [PATCH 529/605] debug: add non-translated debug logging for 'non-whitelisted url' fails --- launcher/InstanceImportTask.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index e3f54aeb..f166088f 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -644,11 +644,15 @@ void InstanceImportTask::processModrinth() file.download = Json::requireString(Json::ensureArray(modInfo, "downloads").first(), "Download URL for " + file.path); - if(!file.download.isValid()) + if (!file.download.isValid()) { + qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(file.download.toString(), file.path); throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); - else if(!Modrinth::validateDownloadUrl(file.download)) + } + else if (!Modrinth::validateDownloadUrl(file.download)) { + qDebug() << QString("Download URL (%1) for %2 is from a non-whitelisted by Modrinth domain").arg(file.download.toString(), file.path); throw JSONValidationError( tr("Download URL for %1 is from a non-whitelisted by Modrinth domain: %2").arg(file.path, file.download.host())); + } files.push_back(file); } From b5e00027d1a16744ae9287b1262e7f6405bd9d5d Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 28 May 2022 14:16:05 -0300 Subject: [PATCH 530/605] change: add 'gitlab.com' to whitelisted Modrinth modpack urls --- launcher/modplatform/modrinth/ModrinthPackManifest.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index b1c4fbcd..8b379480 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -102,6 +102,8 @@ auto validateDownloadUrl(QUrl url) -> bool return true; if(domain == "raw.githubusercontent.com") return true; + if(domain == "gitlab.com") + return true; return false; } From abd240468e362231ce8cbc23573faea9a0e657f4 Mon Sep 17 00:00:00 2001 From: Lenny McLennington Date: Sat, 28 May 2022 19:54:00 +0100 Subject: [PATCH 531/605] clean up validateDownloadUrl --- .../modrinth/ModrinthPackManifest.cpp | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 8b379480..33116231 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -42,6 +42,8 @@ #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include + static ModrinthAPI api; namespace Modrinth { @@ -95,17 +97,15 @@ void loadIndexedVersions(Modpack& pack, QJsonDocument& doc) auto validateDownloadUrl(QUrl url) -> bool { - auto domain = url.host(); - if(domain == "cdn.modrinth.com") - return true; - if(domain == "github.com") - return true; - if(domain == "raw.githubusercontent.com") - return true; - if(domain == "gitlab.com") - return true; + static QSet domainWhitelist{ + "cdn.modrinth.com", + "github.com", + "raw.githubusercontent.com", + "gitlab.com" + }; - return false; + auto domain = url.host(); + return domainWhitelist.contains(domain); } auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion From f0ec165d42fb694f8027fb32f8c6d0867f286ced Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 28 May 2022 18:04:16 -0300 Subject: [PATCH 532/605] feat: add warning of non-whitelisted URLs instead of a hard fail Based on people's votes on Discord :^) --- launcher/InstanceImportTask.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index f166088f..0b97430e 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -585,6 +585,7 @@ void InstanceImportTask::processMultiMC() void InstanceImportTask::processModrinth() { std::vector files; + std::vector non_whitelisted_files; QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion; try { QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); @@ -650,13 +651,29 @@ void InstanceImportTask::processModrinth() } else if (!Modrinth::validateDownloadUrl(file.download)) { qDebug() << QString("Download URL (%1) for %2 is from a non-whitelisted by Modrinth domain").arg(file.download.toString(), file.path); - throw JSONValidationError( - tr("Download URL for %1 is from a non-whitelisted by Modrinth domain: %2").arg(file.path, file.download.host())); + non_whitelisted_files.push_back(file); } files.push_back(file); } + if (!non_whitelisted_files.empty()) { + QString text; + for (const auto& file : non_whitelisted_files) { + text += tr("Filepath: %1
URL: %2
").arg(file.path, file.download.toString()); + } + + auto message_dialog = new ScrollMessageBox(m_parent, tr("Non-whitelisted mods found"), + tr("The following mods have URLs that are not whitelisted by Modrinth.\n" + "Proceed with caution!"), + text); + message_dialog->setModal(true); + if (message_dialog->exec() == QDialog::Rejected) { + emitFailed("Aborted"); + return; + } + } + auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { QString name = it.key(); From e7f35e6ca3b90437ef1fb8b02d31b4fbd47b866b Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sat, 28 May 2022 23:26:47 +0100 Subject: [PATCH 533/605] API: Add settings to support managed packs Managed packs means an installation of a modpack through a modpack provider. Managed packs track their origins (pack platform, name, id), so that in future features can exist around this - such as updating, and reinstalling. --- launcher/BaseInstance.cpp | 49 +++++++++++++++++++++++++++++++++++++++ launcher/BaseInstance.h | 9 +++++++ 2 files changed, 58 insertions(+) diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 2fb31d94..c9394d3f 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * 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 @@ -76,6 +77,14 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerPassthrough(globalSettings->getSetting("ConsoleMaxLines"), nullptr); m_settings->registerPassthrough(globalSettings->getSetting("ConsoleOverflowStop"), nullptr); + + // Managed Packs + m_settings->registerSetting("ManagedPack", false); + m_settings->registerSetting("ManagedPackType", ""); + m_settings->registerSetting("ManagedPackID", ""); + m_settings->registerSetting("ManagedPackName", ""); + m_settings->registerSetting("ManagedPackVersionID", ""); + m_settings->registerSetting("ManagedPackVersionName", ""); } QString BaseInstance::getPreLaunchCommand() @@ -93,6 +102,46 @@ QString BaseInstance::getPostExitCommand() return settings()->get("PostExitCommand").toString(); } +bool BaseInstance::isManagedPack() +{ + return settings()->get("ManagedPack").toBool(); +} + +QString BaseInstance::getManagedPackType() +{ + return settings()->get("ManagedPackType").toString(); +} + +QString BaseInstance::getManagedPackID() +{ + return settings()->get("ManagedPackID").toString(); +} + +QString BaseInstance::getManagedPackName() +{ + return settings()->get("ManagedPackName").toString(); +} + +QString BaseInstance::getManagedPackVersionID() +{ + return settings()->get("ManagedPackVersionID").toString(); +} + +QString BaseInstance::getManagedPackVersionName() +{ + return settings()->get("ManagedPackVersionName").toString(); +} + +void BaseInstance::setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version) +{ + settings()->set("ManagedPack", true); + settings()->set("ManagedPackType", type); + settings()->set("ManagedPackID", id); + settings()->set("ManagedPackName", name); + settings()->set("ManagedPackVersionID", versionId); + settings()->set("ManagedPackVersionName", version); +} + int BaseInstance::getConsoleMaxLines() const { auto lineSetting = settings()->getSetting("ConsoleMaxLines"); diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index c973fcd4..66177614 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * 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 @@ -139,6 +140,14 @@ public: QString getPostExitCommand(); QString getWrapperCommand(); + bool isManagedPack(); + QString getManagedPackType(); + QString getManagedPackID(); + QString getManagedPackName(); + QString getManagedPackVersionID(); + QString getManagedPackVersionName(); + void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version); + /// guess log level from a line of game log virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) { From a98b6663e1fc130b398514fdf3ecb3d4e40b9460 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sun, 22 May 2022 14:34:11 +0100 Subject: [PATCH 534/605] ATLauncher: Pass the full pack name through to the install task --- .../modplatform/atlauncher/ATLPackInstallTask.cpp | 15 ++++++++------- .../modplatform/atlauncher/ATLPackInstallTask.h | 5 +++-- .../ui/pages/modplatform/atlauncher/AtlPage.cpp | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 62c7bf6d..c5477add 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -60,10 +60,11 @@ namespace ATLauncher { static Meta::VersionPtr getComponentVersion(const QString& uid, const QString& version); -PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString pack, QString version) +PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString packName, QString version) { m_support = support; - m_pack = pack; + m_pack_name = packName; + m_pack_safe_name = packName.replace(QRegularExpression("[^A-Za-z0-9]"), ""); m_version_name = version; } @@ -81,7 +82,7 @@ void PackInstallTask::executeTask() qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId(); auto *netJob = new NetJob("ATLauncher::VersionFetch", APPLICATION->network()); auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json") - .arg(m_pack).arg(m_version_name); + .arg(m_pack_safe_name).arg(m_version_name); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); @@ -319,7 +320,7 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); auto f = std::make_shared(); - f->name = m_pack + " " + m_version_name + " (libraries)"; + f->name = m_pack_name + " " + m_version_name + " (libraries)"; const static QMap liteLoaderMap = { { "61179803bcd5fb7790789b790908663d", "1.12-SNAPSHOT" }, @@ -465,7 +466,7 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< } auto f = std::make_shared(); - f->name = m_pack + " " + m_version_name; + f->name = m_pack_name + " " + m_version_name; if (!mainClass.isEmpty() && !mainClasses.contains(mainClass)) { f->mainClass = mainClass; } @@ -507,9 +508,9 @@ void PackInstallTask::installConfigs() setStatus(tr("Downloading configs...")); jobPtr = new NetJob(tr("Config download"), APPLICATION->network()); - auto path = QString("Configs/%1/%2.zip").arg(m_pack).arg(m_version_name); + auto path = QString("Configs/%1/%2.zip").arg(m_pack_safe_name).arg(m_version_name); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip") - .arg(m_pack).arg(m_version_name); + .arg(m_pack_safe_name).arg(m_version_name); auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", path); entry->setStale(true); diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index f0af4e3a..f55873e9 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -75,7 +75,7 @@ class PackInstallTask : public InstanceTask Q_OBJECT public: - explicit PackInstallTask(UserInteractionSupport *support, QString pack, QString version); + explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version); virtual ~PackInstallTask(){} bool canAbort() const override { return true; } @@ -117,7 +117,8 @@ private: NetJob::Ptr jobPtr; QByteArray response; - QString m_pack; + QString m_pack_name; + QString m_pack_safe_name; QString m_version_name; PackVersion m_version; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index 7bc6fc6b..8de5211c 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -117,7 +117,7 @@ void AtlPage::suggestCurrent() return; } - dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.safeName, selectedVersion)); + dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.name, selectedVersion)); auto editedLogoName = selected.safeName; auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower()); listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo) From 411bf3be03ca474f371164c903f95aaa256d81fa Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sat, 28 May 2022 23:38:21 +0100 Subject: [PATCH 535/605] ATLauncher: Make packs managed when installing --- launcher/modplatform/atlauncher/ATLPackInstallTask.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index c5477add..d5bdf1d8 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -863,6 +863,7 @@ void PackInstallTask::install() instance.setName(m_instName); instance.setIconKey(m_instIcon); + instance.setManagedPack("atlauncher", m_pack_safe_name, m_pack_name, m_version_name, m_version_name); instanceSettings->resumeSave(); jarmods.clear(); From 96b76c8f5cd54111303ce642998d8fa020d9c0a5 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sat, 28 May 2022 23:38:55 +0100 Subject: [PATCH 536/605] ModpacksCH: Make packs managed when installing --- .../modpacksch/FTBPackInstallTask.cpp | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 33df6fa4..47143c9d 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield - * Copyright 2020-2021 Petr Mrazek + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2020-2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "FTBPackInstallTask.h" @@ -220,6 +239,7 @@ void PackInstallTask::install() instance.setName(m_instName); instance.setIconKey(m_instIcon); + instance.setManagedPack("modpacksch", QString::number(m_pack.id), m_pack.name, QString::number(m_version.id), m_version.name); instanceSettings->resumeSave(); emitSucceeded(); From febdb85f960f105ac9d85fdafddbe5c0c74673f1 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Mon, 2 May 2022 15:48:12 +0100 Subject: [PATCH 537/605] ModpacksCH: Use ModpacksCH rather than FTB in error messages --- .../modplatform/modpacksch/FTBPackInstallTask.cpp | 2 +- launcher/ui/pages/modplatform/ftb/FtbListModel.cpp | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 47143c9d..c324ffda 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -99,7 +99,7 @@ void PackInstallTask::onDownloadSucceeded() QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp index 37244fed..ad15b6e6 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp @@ -122,10 +122,10 @@ void ListModel::requestFinished() jobPtr.reset(); remainingPacks.clear(); - QJsonParseError parse_error; + QJsonParseError parse_error {}; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } @@ -169,7 +169,7 @@ void ListModel::packRequestFinished() QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } @@ -184,7 +184,7 @@ void ListModel::packRequestFinished() catch (const JSONValidationError &e) { qDebug() << QString::fromUtf8(response); - qWarning() << "Error while reading pack manifest from FTB: " << e.cause(); + qWarning() << "Error while reading pack manifest from ModpacksCH: " << e.cause(); return; } @@ -192,7 +192,7 @@ void ListModel::packRequestFinished() // ignore those "dud" packs. if (pack.versions.empty()) { - qWarning() << "FTB Pack " << pack.id << " ignored. reason: lacking any versions"; + qWarning() << "ModpacksCH Pack " << pack.id << " ignored. reason: lacking any versions"; } else { @@ -270,7 +270,7 @@ void ListModel::requestLogo(QString logo, QString url) bool stale = entry->isStale(); - NetJob *job = new NetJob(QString("FTB Icon Download %1").arg(logo), APPLICATION->network()); + NetJob *job = new NetJob(QString("ModpacksCH Icon Download %1").arg(logo), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); From 80da1f1bb96968d8545ddcd6698da75466bd9934 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sat, 28 May 2022 23:59:00 +0100 Subject: [PATCH 538/605] ATLauncher: Use ATLauncher rather than FTB in error messages --- launcher/modplatform/atlauncher/ATLPackInstallTask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index d5bdf1d8..b4936bd8 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -99,7 +99,7 @@ void PackInstallTask::onDownloadSucceeded() QJsonParseError parse_error {}; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << "Error while parsing JSON response from ATLauncher at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } From d4c1d627814ab4719c7baec56941e8cc1038510e Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Sun, 29 May 2022 12:15:20 +0800 Subject: [PATCH 539/605] Update launcher/ui/pages/global/JavaPage.cpp Co-authored-by: Kenneth Chew <79120643+kthchew@users.noreply.github.com> --- launcher/ui/pages/global/JavaPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index 54bfb3cf..88607e32 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -95,7 +95,7 @@ void JavaPage::applySettings() // Java Settings s->set("JavaPath", ui->javaPathTextBox->text()); - s->set("JvmArgs", ui->jvmArgsTextBox->toPlainText()); + s->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); s->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked()); s->set("IgnoreJavaWizard", ui->skipJavaWizardCheckbox->isChecked()); JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget()); From 577f17c9166ffc5981ab4cfccbba45ed62b38ebe Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sun, 29 May 2022 09:45:20 +0200 Subject: [PATCH 540/605] remove vista support from the manifest we don't support qt <5.12 anymore anyways --- program_info/polymc.manifest | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/program_info/polymc.manifest b/program_info/polymc.manifest index 2d9eb165..8ca50acf 100644 --- a/program_info/polymc.manifest +++ b/program_info/polymc.manifest @@ -16,15 +16,13 @@ Custom Minecraft launcher for managing multiple installs. - - - + From b07c5982e115befaec91a3919dafc9e6ec467f24 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 29 May 2022 12:46:44 +0200 Subject: [PATCH 541/605] fix: set version for Windows binaries --- CMakeLists.txt | 2 ++ launcher/CMakeLists.txt | 2 +- program_info/CMakeLists.txt | 3 +++ program_info/{polymc.manifest => polymc.manifest.in} | 2 +- program_info/{polymc.rc => polymc.rc.in} | 6 +++--- 5 files changed, 10 insertions(+), 5 deletions(-) rename program_info/{polymc.manifest => polymc.manifest.in} (97%) rename program_info/{polymc.rc => polymc.rc.in} (76%) diff --git a/CMakeLists.txt b/CMakeLists.txt index fcc2512d..31b2f23b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -128,6 +128,8 @@ message(STATUS "Git commit: ${Launcher_GIT_COMMIT}") message(STATUS "Git refspec: ${Launcher_GIT_REFSPEC}") set(Launcher_RELEASE_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}") +set(Launcher_RELEASE_VERSION_NAME4 "${Launcher_RELEASE_VERSION_NAME}.0") +set(Launcher_RELEASE_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},${Launcher_VERSION_HOTFIX},0") string(TIMESTAMP TODAY "%Y-%m-%d") set(Launcher_RELEASE_TIMESTAMP "${TODAY}") diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index b3af12a6..bbf80185 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -963,7 +963,7 @@ qt5_add_resources(LAUNCHER_RESOURCES ######## Windows resource files ######## if(WIN32) - set(LAUNCHER_RCS ../${Launcher_Branding_WindowsRC}) + set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC}) endif() # Add executable diff --git a/program_info/CMakeLists.txt b/program_info/CMakeLists.txt index 60549d8d..2cbef1b6 100644 --- a/program_info/CMakeLists.txt +++ b/program_info/CMakeLists.txt @@ -21,3 +21,6 @@ set(Launcher_Portable_File "program_info/portable.txt" PARENT_SCOPE) configure_file(org.polymc.PolyMC.desktop.in org.polymc.PolyMC.desktop) configure_file(org.polymc.PolyMC.metainfo.xml.in org.polymc.PolyMC.metainfo.xml) +configure_file(polymc.rc.in polymc.rc @ONLY) +configure_file(polymc.manifest.in polymc.manifest @ONLY) +configure_file(polymc.ico polymc.ico COPYONLY) diff --git a/program_info/polymc.manifest b/program_info/polymc.manifest.in similarity index 97% rename from program_info/polymc.manifest rename to program_info/polymc.manifest.in index 8ca50acf..0eefacac 100644 --- a/program_info/polymc.manifest +++ b/program_info/polymc.manifest.in @@ -1,6 +1,6 @@ - + diff --git a/program_info/polymc.rc b/program_info/polymc.rc.in similarity index 76% rename from program_info/polymc.rc rename to program_info/polymc.rc.in index 011e944b..0ea9b73a 100644 --- a/program_info/polymc.rc +++ b/program_info/polymc.rc.in @@ -7,7 +7,7 @@ IDI_ICON1 ICON DISCARDABLE "polymc.ico" 1 RT_MANIFEST "polymc.manifest" VS_VERSION_INFO VERSIONINFO -FILEVERSION 1,0,0,0 +FILEVERSION @Launcher_RELEASE_VERSION_NAME4_COMMA@ FILEOS VOS_NT_WINDOWS32 FILETYPE VFT_APP BEGIN @@ -17,9 +17,9 @@ BEGIN BEGIN VALUE "CompanyName", "MultiMC & PolyMC Contributors" VALUE "FileDescription", "PolyMC" - VALUE "FileVersion", "1.0.0.0" + VALUE "FileVersion", "@Launcher_RELEASE_VERSION_NAME4@" VALUE "ProductName", "PolyMC" - VALUE "ProductVersion", "1" + VALUE "ProductVersion", "@Launcher_RELEASE_VERSION_NAME4@" END END BLOCK "VarFileInfo" From 0b3115997a970b0507665703b9e70da8b3d22423 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 29 May 2022 14:16:13 +0200 Subject: [PATCH 542/605] fix: fix importing Flame/MMC packs --- launcher/InstanceImportTask.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 68a497cc..6df5a491 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -164,14 +164,14 @@ void InstanceImportTask::processZipPack() QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json"); - if (!mmcRoot.isEmpty()) + if (!mmcRoot.isNull()) { // process as MultiMC instance/pack qDebug() << "MultiMC:" << mmcRoot; root = mmcRoot; m_modpackType = ModpackType::MultiMC; } - else if(!flameRoot.isEmpty()) + else if(!flameRoot.isNull()) { // process as Flame pack qDebug() << "Flame:" << flameRoot; From 8e6c592ad9add4f8241c54a64a63ba1cc3750af1 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 29 May 2022 14:28:54 +0200 Subject: [PATCH 543/605] fix: add version to Legacy FTB packs --- launcher/ui/pages/modplatform/legacy_ftb/Page.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 27a12cda..7667d169 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -175,7 +175,7 @@ void Page::suggestCurrent() return; } - dialog->setSuggestedPack(selected.name, new PackInstallTask(APPLICATION->network(), selected, selectedVersion)); + dialog->setSuggestedPack(selected.name + " " + selectedVersion, new PackInstallTask(APPLICATION->network(), selected, selectedVersion)); QString editedLogoName; if(selected.logo.toLower().startsWith("ftb")) { From 20832682efc4205f350c27c1d8aa6681925c76e6 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Sun, 29 May 2022 20:35:57 +0800 Subject: [PATCH 544/605] Update launcher/ui/pages/global/JavaPage.cpp Co-authored-by: Sefa Eyeoglu --- launcher/ui/pages/global/JavaPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index 88607e32..025771e8 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -166,7 +166,7 @@ void JavaPage::on_javaTestBtn_clicked() return; } checker.reset(new JavaCommon::TestCheck( - this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->toPlainText(), + 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(); From ee00a5d8eef6c9239d7701554d065c0f5f8767c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20=C3=87al=C4=B1=C5=9Fkan?= Date: Sun, 29 May 2022 17:07:12 +0300 Subject: [PATCH 545/605] nix: make LTO optional --- nix/default.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nix/default.nix b/nix/default.nix index cce40e63..969b455e 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -11,6 +11,7 @@ , xorg , libpulseaudio , qtbase +, quazip , libGL , msaClientID ? "" @@ -18,6 +19,7 @@ , self , version , libnbtplusplus +, enableLTO ? false }: let @@ -57,9 +59,9 @@ stdenv.mkDerivation rec { cmakeFlags = [ "-GNinja" - "-DENABLE_LTO=on" "-DLauncher_QT_VERSION_MAJOR=${lib.versions.major qtbase.version}" - ] ++ lib.optionals (msaClientID != "") [ "-DLauncher_MSA_CLIENT_ID=${msaClientID}" ]; + ] ++ lib.optionals enableLTO [ "-DENABLE_LTO=on" ] + ++ lib.optionals (msaClientID != "") [ "-DLauncher_MSA_CLIENT_ID=${msaClientID}" ]; postInstall = '' # xorg.xrandr needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 From adf1e1982a66959aa594bb0c43eba5428f88999d Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 29 May 2022 16:14:01 +0200 Subject: [PATCH 546/605] fix: remove unnecessary translation (#674) --- launcher/ui/dialogs/ScrollMessageBox.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/ScrollMessageBox.ui b/launcher/ui/dialogs/ScrollMessageBox.ui index 885fbfd2..299d2ecc 100644 --- a/launcher/ui/dialogs/ScrollMessageBox.ui +++ b/launcher/ui/dialogs/ScrollMessageBox.ui @@ -11,7 +11,7 @@ - ScrollMessageBox + ScrollMessageBox From 2746251dcd7ccbe83ca969c6a6b0f6e9c4d9160d Mon Sep 17 00:00:00 2001 From: timoreo Date: Sun, 29 May 2022 18:23:34 +0200 Subject: [PATCH 547/605] Fix modrinth search filters --- launcher/modplatform/ModAPI.h | 2 +- launcher/modplatform/modrinth/ModrinthAPI.h | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index 4230df0b..eb0de3f0 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -68,7 +68,7 @@ class ModAPI { { QString s; for(auto& ver : mcVersions){ - s += QString("%1,").arg(ver.toString()); + s += QString("\"%1\",").arg(ver.toString()); } s.remove(s.length() - 1, 1); //remove last comma return s; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 79bc5175..6119a4df 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -79,11 +79,11 @@ class ModrinthAPI : public NetworkModAPI { { return QString(BuildConfig.MODRINTH_PROD_URL + "/project/%1/version?" - "game_versions=[%2]" + "game_versions=[%2]&" "loaders=[\"%3\"]") - .arg(args.addonId) - .arg(getGameVersionsString(args.mcVersions)) - .arg(getModLoaderStrings(args.loaders).join("\",\"")); + .arg(args.addonId, + getGameVersionsString(args.mcVersions), + getModLoaderStrings(args.loaders).join("\",\"")); }; auto getGameVersionsArray(std::list mcVersions) const -> QString From 8731c86d0deba2f8e624d41137b69abca5b85e8e Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 29 May 2022 17:37:45 -0400 Subject: [PATCH 548/605] Use CMake for Windows installer branding As a side effect, fixes an issue where the installer wrote the incorrect version to the registry. --- .github/workflows/build.yml | 2 +- CMakeLists.txt | 2 + program_info/CMakeLists.txt | 1 + .../{win_install.nsi => win_install.nsi.in} | 45 ++++++++++--------- 4 files changed, 28 insertions(+), 22 deletions(-) rename program_info/{win_install.nsi => win_install.nsi.in} (81%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6cbd5c21..db7bd653 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -206,7 +206,7 @@ jobs: shell: msys2 {0} run: | cd ${{ env.INSTALL_DIR }} - makensis -NOCD "-DVERSION=${{ env.VERSION }}" "-DMUI_ICON=${{ github.workspace }}/program_info/polymc.ico" "-XOutFile ${{ github.workspace }}/PolyMC-Setup.exe" "${{ github.workspace }}/program_info/win_install.nsi" + makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi" - name: Package (Linux) if: runner.os == 'Linux' && matrix.appimage != true diff --git a/CMakeLists.txt b/CMakeLists.txt index 11d58213..4e9e2e5a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -260,6 +260,8 @@ elseif(WIN32) # install as bundle set(INSTALL_BUNDLE "full") + + configure_file(program_info/win_install.nsi.in program_info/win_install.nsi @ONLY) else() message(FATAL_ERROR "Platform not supported") endif() diff --git a/program_info/CMakeLists.txt b/program_info/CMakeLists.txt index 2cbef1b6..b2325b6f 100644 --- a/program_info/CMakeLists.txt +++ b/program_info/CMakeLists.txt @@ -14,6 +14,7 @@ set(Launcher_MetaInfo "program_info/org.polymc.PolyMC.metainfo.xml" PARENT_SCOPE set(Launcher_ManPage "program_info/polymc.6.txt" PARENT_SCOPE) set(Launcher_SVG "program_info/org.polymc.PolyMC.svg" PARENT_SCOPE) set(Launcher_Branding_ICNS "program_info/polymc.icns" PARENT_SCOPE) +set(Launcher_Branding_ICO "program_info/polymc.ico" PARENT_SCOPE) set(Launcher_Branding_WindowsRC "program_info/polymc.rc" PARENT_SCOPE) set(Launcher_Branding_LogoQRC "program_info/polymc.qrc" PARENT_SCOPE) diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi.in similarity index 81% rename from program_info/win_install.nsi rename to program_info/win_install.nsi.in index cb4c8d1d..d8b3e88f 100644 --- a/program_info/win_install.nsi +++ b/program_info/win_install.nsi.in @@ -4,10 +4,13 @@ Unicode true -Name "PolyMC" -InstallDir "$LOCALAPPDATA\Programs\PolyMC" -InstallDirRegKey HKCU "Software\PolyMC" "InstallDir" +Name "@Launcher_Name@" +InstallDir "$LOCALAPPDATA\Programs\@Launcher_Name@" +InstallDirRegKey HKCU "Software\@Launcher_Name@" "InstallDir" RequestExecutionLevel user +OutFile "../@Launcher_Name@-Setup.exe" + +!define MUI_ICON "../@Launcher_Branding_ICO@" ;-------------------------------- @@ -18,7 +21,7 @@ RequestExecutionLevel user !insertmacro MUI_PAGE_COMPONENTS !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES -!define MUI_FINISHPAGE_RUN "$InstDir\polymc.exe" +!define MUI_FINISHPAGE_RUN "$InstDir\@Launcher_APP_BINARY_NAME@.exe" !insertmacro MUI_PAGE_FINISH !insertmacro MUI_UNPAGE_CONFIRM @@ -99,15 +102,15 @@ RequestExecutionLevel user ;-------------------------------- ; The stuff to install -Section "PolyMC" +Section "@Launcher_Name@" SectionIn RO - nsExec::Exec /TIMEOUT=2000 'TaskKill /IM polymc.exe /F' + nsExec::Exec /TIMEOUT=2000 'TaskKill /IM @Launcher_APP_BINARY_NAME@.exe /F' SetOutPath $INSTDIR - File "polymc.exe" + File "@Launcher_APP_BINARY_NAME@.exe" File "qt.conf" File *.dll File /r "iconengines" @@ -117,20 +120,20 @@ Section "PolyMC" File /r "styles" ; Write the installation path into the registry - WriteRegStr HKCU Software\PolyMC "InstallDir" "$INSTDIR" + WriteRegStr HKCU Software\@Launcher_Name@ "InstallDir" "$INSTDIR" ; Write the uninstall keys for Windows ${GetParameters} $R0 ${GetOptions} $R0 "/NoUninstaller" $R1 ${If} ${Errors} - !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" - WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "PolyMC" - WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\polymc.exe" + !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\@Launcher_Name@" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "@Launcher_Name@" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe"' WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S' WriteRegStr HKCU "${UNINST_KEY}" "InstallLocation" "$INSTDIR" - WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "PolyMC Contributors" - WriteRegStr HKCU "${UNINST_KEY}" "ProductVersion" "${VERSION}" + WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "@Launcher_Name@ Contributors" + WriteRegStr HKCU "${UNINST_KEY}" "ProductVersion" "@Launcher_RELEASE_VERSION_NAME@" ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 IntFmt $0 "0x%08X" $0 WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0" @@ -143,13 +146,13 @@ SectionEnd Section "Start Menu Shortcut" SM_SHORTCUTS - CreateShortcut "$SMPROGRAMS\PolyMC.lnk" "$INSTDIR\polymc.exe" "" "$INSTDIR\polymc.exe" 0 + CreateShortcut "$SMPROGRAMS\@Launcher_Name@.lnk" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" 0 SectionEnd Section "Desktop Shortcut" DESKTOP_SHORTCUTS - CreateShortcut "$DESKTOP\PolyMC.lnk" "$INSTDIR\polymc.exe" "" "$INSTDIR\polymc.exe" 0 + CreateShortcut "$DESKTOP\@Launcher_Name@.lnk" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" 0 SectionEnd @@ -159,12 +162,12 @@ SectionEnd Section "Uninstall" - nsExec::Exec /TIMEOUT=2000 'TaskKill /IM polymc.exe /F' + nsExec::Exec /TIMEOUT=2000 'TaskKill /IM @Launcher_APP_BINARY_NAME@.exe /F' - DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" - DeleteRegKey HKCU SOFTWARE\PolyMC + DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\@Launcher_Name@" + DeleteRegKey HKCU SOFTWARE\@Launcher_Name@ - Delete $INSTDIR\polymc.exe + Delete $INSTDIR\@Launcher_APP_BINARY_NAME@.exe Delete $INSTDIR\uninstall.exe Delete $INSTDIR\portable.txt @@ -220,8 +223,8 @@ Section "Uninstall" RMDir /r $INSTDIR\platforms RMDir /r $INSTDIR\styles - Delete "$SMPROGRAMS\PolyMC.lnk" - Delete "$DESKTOP\PolyMC.lnk" + Delete "$SMPROGRAMS\@Launcher_Name@.lnk" + Delete "$DESKTOP\@Launcher_Name@.lnk" RMDir "$INSTDIR" From 9d8b95107da69cb0202824e6e5d7211b3a7e2830 Mon Sep 17 00:00:00 2001 From: Ilia Date: Mon, 30 May 2022 13:33:07 +0300 Subject: [PATCH 549/605] fix: do not show the "profile select" dialog if the user refused to add an account --- launcher/LaunchController.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 002c08b9..d36ee3fe 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -105,6 +105,11 @@ void LaunchController::decideAccount() // Open the account manager. APPLICATION->ShowGlobalSettings(m_parentWidget, "accounts"); } + else if (reply == QMessageBox::No) + { + // Do not open "profile select" dialog. + return; + } } m_accountToUse = accounts->defaultAccount(); From 065e38c6aa4ebde6e256b02306758d645daf525e Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 30 May 2022 12:56:29 -0400 Subject: [PATCH 550/605] Move Windows installer configure to `program_info` --- CMakeLists.txt | 2 -- program_info/CMakeLists.txt | 4 +++- program_info/win_install.nsi.in | 30 +++++++++++++++--------------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e9e2e5a..11d58213 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -260,8 +260,6 @@ elseif(WIN32) # install as bundle set(INSTALL_BUNDLE "full") - - configure_file(program_info/win_install.nsi.in program_info/win_install.nsi @ONLY) else() message(FATAL_ERROR "Platform not supported") endif() diff --git a/program_info/CMakeLists.txt b/program_info/CMakeLists.txt index b2325b6f..1000be23 100644 --- a/program_info/CMakeLists.txt +++ b/program_info/CMakeLists.txt @@ -14,7 +14,8 @@ set(Launcher_MetaInfo "program_info/org.polymc.PolyMC.metainfo.xml" PARENT_SCOPE set(Launcher_ManPage "program_info/polymc.6.txt" PARENT_SCOPE) set(Launcher_SVG "program_info/org.polymc.PolyMC.svg" PARENT_SCOPE) set(Launcher_Branding_ICNS "program_info/polymc.icns" PARENT_SCOPE) -set(Launcher_Branding_ICO "program_info/polymc.ico" PARENT_SCOPE) +set(Launcher_Branding_ICO "program_info/polymc.ico") +set(Launcher_Branding_ICO "${Launcher_Branding_ICO}" PARENT_SCOPE) set(Launcher_Branding_WindowsRC "program_info/polymc.rc" PARENT_SCOPE) set(Launcher_Branding_LogoQRC "program_info/polymc.qrc" PARENT_SCOPE) @@ -25,3 +26,4 @@ configure_file(org.polymc.PolyMC.metainfo.xml.in org.polymc.PolyMC.metainfo.xml) configure_file(polymc.rc.in polymc.rc @ONLY) configure_file(polymc.manifest.in polymc.manifest @ONLY) configure_file(polymc.ico polymc.ico COPYONLY) +configure_file(win_install.nsi.in win_install.nsi @ONLY) diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index d8b3e88f..596e3b57 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -4,11 +4,11 @@ Unicode true -Name "@Launcher_Name@" -InstallDir "$LOCALAPPDATA\Programs\@Launcher_Name@" -InstallDirRegKey HKCU "Software\@Launcher_Name@" "InstallDir" +Name "@Launcher_CommonName@" +InstallDir "$LOCALAPPDATA\Programs\@Launcher_CommonName@" +InstallDirRegKey HKCU "Software\@Launcher_CommonName@" "InstallDir" RequestExecutionLevel user -OutFile "../@Launcher_Name@-Setup.exe" +OutFile "../@Launcher_CommonName@-Setup.exe" !define MUI_ICON "../@Launcher_Branding_ICO@" @@ -102,7 +102,7 @@ OutFile "../@Launcher_Name@-Setup.exe" ;-------------------------------- ; The stuff to install -Section "@Launcher_Name@" +Section "@Launcher_CommonName@" SectionIn RO @@ -120,19 +120,19 @@ Section "@Launcher_Name@" File /r "styles" ; Write the installation path into the registry - WriteRegStr HKCU Software\@Launcher_Name@ "InstallDir" "$INSTDIR" + WriteRegStr HKCU Software\@Launcher_CommonName@ "InstallDir" "$INSTDIR" ; Write the uninstall keys for Windows ${GetParameters} $R0 ${GetOptions} $R0 "/NoUninstaller" $R1 ${If} ${Errors} - !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\@Launcher_Name@" - WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "@Launcher_Name@" + !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\@Launcher_CommonName@" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "@Launcher_CommonName@" WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe"' WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S' WriteRegStr HKCU "${UNINST_KEY}" "InstallLocation" "$INSTDIR" - WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "@Launcher_Name@ Contributors" + WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "@Launcher_CommonName@ Contributors" WriteRegStr HKCU "${UNINST_KEY}" "ProductVersion" "@Launcher_RELEASE_VERSION_NAME@" ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 IntFmt $0 "0x%08X" $0 @@ -146,13 +146,13 @@ SectionEnd Section "Start Menu Shortcut" SM_SHORTCUTS - CreateShortcut "$SMPROGRAMS\@Launcher_Name@.lnk" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" 0 + CreateShortcut "$SMPROGRAMS\@Launcher_CommonName@.lnk" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" 0 SectionEnd Section "Desktop Shortcut" DESKTOP_SHORTCUTS - CreateShortcut "$DESKTOP\@Launcher_Name@.lnk" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" 0 + CreateShortcut "$DESKTOP\@Launcher_CommonName@.lnk" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" 0 SectionEnd @@ -164,8 +164,8 @@ Section "Uninstall" nsExec::Exec /TIMEOUT=2000 'TaskKill /IM @Launcher_APP_BINARY_NAME@.exe /F' - DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\@Launcher_Name@" - DeleteRegKey HKCU SOFTWARE\@Launcher_Name@ + DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\@Launcher_CommonName@" + DeleteRegKey HKCU SOFTWARE\@Launcher_CommonName@ Delete $INSTDIR\@Launcher_APP_BINARY_NAME@.exe Delete $INSTDIR\uninstall.exe @@ -223,8 +223,8 @@ Section "Uninstall" RMDir /r $INSTDIR\platforms RMDir /r $INSTDIR\styles - Delete "$SMPROGRAMS\@Launcher_Name@.lnk" - Delete "$DESKTOP\@Launcher_Name@.lnk" + Delete "$SMPROGRAMS\@Launcher_CommonName@.lnk" + Delete "$DESKTOP\@Launcher_CommonName@.lnk" RMDir "$INSTDIR" From 3585e4764b1bbd6e3e90d322f51ddb9d49a2ceec Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 30 May 2022 14:01:38 -0400 Subject: [PATCH 551/605] Add Quilt support for Technic modpacks --- .../technic/TechnicPackProcessor.cpp | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index 782fb9b2..f50e5fb5 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -185,13 +185,22 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1)); } } - else if (libraryName.startsWith("net.minecraftforge:minecraftforge:")) + else { - components->setComponentVersion("net.minecraftforge", libraryName.section(':', 2)); - } - else if (libraryName.startsWith("net.fabricmc:fabric-loader:")) - { - components->setComponentVersion("net.fabricmc.fabric-loader", libraryName.section(':', 2)); + static QSet possibleLoaders{ + "net.minecraftforge:minecraftforge:", + "net.fabricmc:fabric-loader:", + "org.quiltmc:quilt-loader:" + }; + for (const auto& loader : possibleLoaders) + { + if (libraryName.startsWith(loader)) + { + auto loaderComponent = loader.chopped(1).replace(":", "."); + components->setComponentVersion(loaderComponent, libraryName.section(':', 2)); + break; + } + } } } } From 7ac16ed0734168793dba4c09ed2e600cd6c92fee Mon Sep 17 00:00:00 2001 From: Kenneth Chew <79120643+kthchew@users.noreply.github.com> Date: Mon, 30 May 2022 14:40:20 -0400 Subject: [PATCH 552/605] Use `QStringList` instead of `QSet` Co-authored-by: flow --- launcher/modplatform/technic/TechnicPackProcessor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index f50e5fb5..471b4a2f 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -187,7 +187,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const } else { - static QSet possibleLoaders{ + static QStringList possibleLoaders{ "net.minecraftforge:minecraftforge:", "net.fabricmc:fabric-loader:", "org.quiltmc:quilt-loader:" From 2999e69f0ff1a8e09d7ef625f73bb3559e181e69 Mon Sep 17 00:00:00 2001 From: LennyMcLennington Date: Mon, 30 May 2022 23:05:29 +0100 Subject: [PATCH 553/605] Change forking policy a bit Ask people forking PolyMC to make it clear that their fork is not endorsed by us. --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a08d5dc0..e4b9ebcd 100644 --- a/README.md +++ b/README.md @@ -80,12 +80,17 @@ To modify download information or change packaging information send a pull reque ## Forking/Redistributing/Custom builds policy -Do whatever you want, we don't care. Just follow the license. If you have any questions about this feel free to ask in an issue. +We don't care what you do with your fork as long as you do the following as a basic courtesy: +- Follow the terms of the [license](LICENSE) (not just a courtesy, but also a legal responsibility) +- Make it clear that your fork is not PolyMC and is not endorsed by or affiliated with the PolyMC project (https://polymc.org). +- If you are distributing this fork, go through [CMakeLists.txt](CMakeLists.txt) and change PolyMC's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring to those keys will be disabled). + +If you have any questions or want any clarification on the above conditions please make an issue and ask us. Be aware that if you build this software without removing the provided API keys in [CMakeLists.txt](CMakeLists.txt) you are accepting the following terms and conditions: - [Microsoft Identity Platform Terms of Use](https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use) - [CurseForge 3rd Party API Terms and Conditions](https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions) -If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file. +If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file by setting them to an empty string (`""`). All launcher code is available under the GPL-3.0-only license. From 8ce8aadd9b7f9da5ef09e1e36e913f12928f3ca9 Mon Sep 17 00:00:00 2001 From: LennyMcLennington Date: Mon, 30 May 2022 23:50:35 +0100 Subject: [PATCH 554/605] fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e4b9ebcd..bdbe5f8d 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ To modify download information or change packaging information send a pull reque We don't care what you do with your fork as long as you do the following as a basic courtesy: - Follow the terms of the [license](LICENSE) (not just a courtesy, but also a legal responsibility) - Make it clear that your fork is not PolyMC and is not endorsed by or affiliated with the PolyMC project (https://polymc.org). -- If you are distributing this fork, go through [CMakeLists.txt](CMakeLists.txt) and change PolyMC's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring to those keys will be disabled). +- If you are distributing this fork, go through [CMakeLists.txt](CMakeLists.txt) and change PolyMC's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring those keys will be disabled). If you have any questions or want any clarification on the above conditions please make an issue and ask us. From 2727df704f19d34e2169b3899c2646917fc4a594 Mon Sep 17 00:00:00 2001 From: LennyMcLennington Date: Mon, 30 May 2022 23:52:12 +0100 Subject: [PATCH 555/605] change the wording a bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bdbe5f8d..86c5dace 100644 --- a/README.md +++ b/README.md @@ -80,10 +80,10 @@ To modify download information or change packaging information send a pull reque ## Forking/Redistributing/Custom builds policy -We don't care what you do with your fork as long as you do the following as a basic courtesy: +We don't care what you do with your fork/custom build as long as you do the following as a basic courtesy: - Follow the terms of the [license](LICENSE) (not just a courtesy, but also a legal responsibility) - Make it clear that your fork is not PolyMC and is not endorsed by or affiliated with the PolyMC project (https://polymc.org). -- If you are distributing this fork, go through [CMakeLists.txt](CMakeLists.txt) and change PolyMC's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring those keys will be disabled). +- Go through [CMakeLists.txt](CMakeLists.txt) and change PolyMC's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring those keys will be disabled). If you have any questions or want any clarification on the above conditions please make an issue and ask us. From 795075f90d411338d5b92723bccb790815202b0d Mon Sep 17 00:00:00 2001 From: LennyMcLennington Date: Mon, 30 May 2022 23:59:48 +0100 Subject: [PATCH 556/605] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 86c5dace..a5cc154f 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ If you have any questions or want any clarification on the above conditions plea Be aware that if you build this software without removing the provided API keys in [CMakeLists.txt](CMakeLists.txt) you are accepting the following terms and conditions: - [Microsoft Identity Platform Terms of Use](https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use) - [CurseForge 3rd Party API Terms and Conditions](https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions) + If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file by setting them to an empty string (`""`). All launcher code is available under the GPL-3.0-only license. From 7d21bf15e88517eb3a6e8e4de712a827f87fde39 Mon Sep 17 00:00:00 2001 From: glowiak <52356948+glowiak@users.noreply.github.com> Date: Wed, 1 Jun 2022 15:50:02 +0200 Subject: [PATCH 557/605] Update UpdateController.cpp --- launcher/UpdateController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/UpdateController.cpp b/launcher/UpdateController.cpp index 646f8e57..c27fe772 100644 --- a/launcher/UpdateController.cpp +++ b/launcher/UpdateController.cpp @@ -93,7 +93,7 @@ void UpdateController::installUpdates() qDebug() << "Installing updates."; #ifdef Q_OS_WIN QString finishCmd = QApplication::applicationFilePath(); -#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined (Q_OS_OPENBSD) +#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) QString finishCmd = FS::PathCombine(m_root, BuildConfig.LAUNCHER_NAME); #elif defined Q_OS_MAC QString finishCmd = QApplication::applicationFilePath(); From 1a004f0c4d624453e4f477d025aa46a8a8d47ce4 Mon Sep 17 00:00:00 2001 From: glowiak <52356948+glowiak@users.noreply.github.com> Date: Wed, 1 Jun 2022 15:50:43 +0200 Subject: [PATCH 558/605] Update MCEditTool.cpp --- launcher/tools/MCEditTool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/tools/MCEditTool.cpp b/launcher/tools/MCEditTool.cpp index 2c1ec613..21e1a3b0 100644 --- a/launcher/tools/MCEditTool.cpp +++ b/launcher/tools/MCEditTool.cpp @@ -52,7 +52,7 @@ QString MCEditTool::getProgramPath() #else const QString mceditPath = path(); QDir mceditDir(mceditPath); -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) if (mceditDir.exists("mcedit.sh")) { return mceditDir.absoluteFilePath("mcedit.sh"); From ca21b31696c548a9c03db8932c755030a7d5d116 Mon Sep 17 00:00:00 2001 From: jn64 <23169302+jn64@users.noreply.github.com> Date: Fri, 3 Jun 2022 09:58:57 +0800 Subject: [PATCH 559/605] Add "mc" keyword to desktop file Certain launchers (e.g. GNOME) only search from word boundaries, so typing "mc" doesn't currently match to "PolyMC". --- program_info/org.polymc.PolyMC.desktop.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program_info/org.polymc.PolyMC.desktop.in b/program_info/org.polymc.PolyMC.desktop.in index 2d9e7103..e6d88909 100644 --- a/program_info/org.polymc.PolyMC.desktop.in +++ b/program_info/org.polymc.PolyMC.desktop.in @@ -8,5 +8,5 @@ Exec=@Launcher_APP_BINARY_NAME@ StartupNotify=true Icon=org.polymc.PolyMC Categories=Game; -Keywords=game;minecraft;launcher; +Keywords=game;minecraft;launcher;mc; StartupWMClass=PolyMC From cf4949b4f5a29757b3dd24cdca3a010f10e6dadb Mon Sep 17 00:00:00 2001 From: TheOPtimal <41379516+TheOPtimal@users.noreply.github.com> Date: Sat, 4 Jun 2022 05:26:46 +0400 Subject: [PATCH 560/605] Prepare for Nix 2.7 (#286) * Prepare for Nix 2.7 * Fix embarassing oopsie --- flake.nix | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/flake.nix b/flake.nix index f2247bed..b378fbb0 100644 --- a/flake.nix +++ b/flake.nix @@ -22,15 +22,17 @@ pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system}); in { - packages = forAllSystems (system: { + packages = forAllSystems (system: rec { polymc = pkgs.${system}.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus; }; polymc-qt6 = pkgs.${system}.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus; }; + + default = polymc; }); - defaultPackage = forAllSystems (system: self.packages.${system}.polymc); + defaultPackage = forAllSystems (system: self.packages.${system}.default); - apps = forAllSystems (system: { polymc = { type = "app"; program = "${self.defaultPackage.${system}}/bin/polymc"; }; }); - defaultApp = forAllSystems (system: self.apps.${system}.polymc); + apps = forAllSystems (system: rec { polymc = { type = "app"; program = "${self.defaultPackage.${system}}/bin/polymc"; }; default = polymc; }); + defaultApp = forAllSystems (system: self.apps.${system}.default); overlay = final: prev: { polymc = self.defaultPackage.${final.system}; }; }; From 25ab121e42f624352bb4f32faa29e9e455328f09 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Sat, 4 Jun 2022 15:33:17 +0800 Subject: [PATCH 561/605] feat: custom user-agent --- launcher/Application.cpp | 21 ++++++++++++++++ launcher/Application.h | 2 ++ launcher/net/Download.cpp | 2 +- launcher/net/PasteUpload.cpp | 4 +-- launcher/net/Upload.cpp | 2 +- launcher/screenshots/ImgurAlbumCreation.cpp | 2 +- launcher/screenshots/ImgurUpload.cpp | 3 ++- launcher/ui/pages/global/APIPage.cpp | 4 +++ launcher/ui/pages/global/APIPage.ui | 27 ++++++++++++++++++++- 9 files changed, 60 insertions(+), 7 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ba4096b6..dd6f8ec6 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -708,6 +708,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Custom MSA credentials m_settings->registerSetting("MSAClientIDOverride", ""); m_settings->registerSetting("CFKeyOverride", ""); + m_settings->registerSetting("UserAgentOverride", ""); // Init page provider { @@ -1553,3 +1554,23 @@ QString Application::getCurseKey() return BuildConfig.CURSEFORGE_API_KEY; } + +QString Application::getUserAgent() +{ + QString keyOverride = m_settings->get("UserAgentOverride").toString(); + if (!keyOverride.isEmpty()) { + return keyOverride; + } + + return BuildConfig.USER_AGENT; +} + +QString Application::getUserAgentUncached() +{ + QString keyOverride = m_settings->get("UserAgentOverride").toString(); + if (!keyOverride.isEmpty()) { + return keyOverride; + } + + return BuildConfig.USER_AGENT_UNCACHED; +} diff --git a/launcher/Application.h b/launcher/Application.h index 3129b4fb..f440f433 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -156,6 +156,8 @@ public: QString getMSAClientID(); QString getCurseKey(); + QString getUserAgent(); + QString getUserAgentUncached(); /// this is the root of the 'installation'. Used for automatic updates const QString &root() { diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 966d4126..d93eb088 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -116,7 +116,7 @@ void Download::executeTask() return; } - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); if (request.url().host().contains("api.curseforge.com")) { request.setRawHeader("x-api-key", APPLICATION->getCurseKey().toUtf8()); }; diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 3855190a..ead5e170 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -71,7 +71,7 @@ void PasteUpload::executeTask() QNetworkRequest request{QUrl(m_uploadUrl)}; QNetworkReply *rep{}; - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); switch (m_pasteType) { case NullPointer: { @@ -91,7 +91,7 @@ void PasteUpload::executeTask() break; } case Hastebin: { - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); rep = APPLICATION->network()->post(request, m_text); break; } diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp index bbd27390..c9942a8d 100644 --- a/launcher/net/Upload.cpp +++ b/launcher/net/Upload.cpp @@ -173,7 +173,7 @@ namespace Net { return; } - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); if (request.url().host().contains("api.curseforge.com")) { request.setRawHeader("x-api-key", APPLICATION->getCurseKey().toUtf8()); } diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index 7afdc5cc..04e26ea2 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -55,7 +55,7 @@ void ImgurAlbumCreation::executeTask() { m_state = State::Running; QNetworkRequest request(m_url); - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); request.setRawHeader("Accept", "application/json"); diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index fbcfb95f..9aeb6fb8 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -35,6 +35,7 @@ #include "ImgurUpload.h" #include "BuildConfig.h" +#include "Application.h" #include #include @@ -56,7 +57,7 @@ void ImgurUpload::executeTask() finished = false; m_state = Task::State::Running; QNetworkRequest request(m_url); - request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); request.setRawHeader("Accept", "application/json"); diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 5d812d07..0c1d7ca2 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -78,6 +78,7 @@ APIPage::APIPage(QWidget *parent) : ui->tabWidget->tabBar()->hide(); ui->metaURL->setPlaceholderText(BuildConfig.META_URL); + ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT); loadSettings(); @@ -139,6 +140,8 @@ void APIPage::loadSettings() ui->metaURL->setText(metaURL); QString curseKey = s->get("CFKeyOverride").toString(); ui->curseKey->setText(curseKey); + QString customUserAgent = s->get("UserAgentOverride").toString(); + ui->userAgentLineEdit->setText(customUserAgent); } void APIPage::applySettings() @@ -167,6 +170,7 @@ void APIPage::applySettings() s->set("MetaURLOverride", metaURL); QString curseKey = ui->curseKey->text(); s->set("CFKeyOverride", curseKey); + s->set("UserAgentOverride", ui->userAgentLineEdit->text()); } bool APIPage::apply() diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 5c927391..0981c700 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -7,7 +7,7 @@ 0 0 800 - 600 + 702 @@ -220,6 +220,31 @@ + + + + + 0 + 0 + + + + User Agent + + + + + + + + + Enter a custom User Agent here. The special string ${launcher_version} will be replaced with the version of the launcher. + + + + + + From 778baa6dbe9a7710b86771262bbe435bdd6ee574 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 4 Jun 2022 11:59:12 +0200 Subject: [PATCH 562/605] fix: always store InstanceType --- launcher/BaseInstance.cpp | 2 +- launcher/InstanceList.cpp | 16 ++++++++++++++-- launcher/minecraft/MinecraftInstance.cpp | 2 ++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 2fb31d94..0240afa8 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -59,7 +59,7 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("lastLaunchTime", 0); m_settings->registerSetting("totalTimePlayed", 0); m_settings->registerSetting("lastTimePlayed", 0); - m_settings->registerSetting("InstanceType", "OneSix"); + m_settings->registerSetting("InstanceType", ""); // Custom Commands auto commandSetting = m_settings->registerSetting({"OverrideCommands","OverrideLaunchCmd"}, false); diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 847d897e..3e3c81f7 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -547,8 +547,20 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) auto instanceRoot = FS::PathCombine(m_instDir, id); auto instanceSettings = std::make_shared(FS::PathCombine(instanceRoot, "instance.cfg")); InstancePtr inst; - // TODO: Handle incompatible instances - inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); + + instanceSettings->registerSetting("InstanceType", ""); + + QString inst_type = instanceSettings->get("InstanceType").toString(); + + // NOTE: Some PolyMC 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)); + } + else + { + inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot)); + } qDebug() << "Loaded instance " << inst->name() << " from " << inst->instanceRoot(); return inst; } diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 61326fac..9ec4c17a 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -168,6 +168,8 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO m_settings->registerOverride(globalSettings->getSetting("CloseAfterLaunch"), miscellaneousOverride); m_settings->registerOverride(globalSettings->getSetting("QuitAfterGameStop"), miscellaneousOverride); + m_settings->set("InstanceType", "OneSix"); + m_components.reset(new PackProfile(this)); } From c2a43c6f40d4fd24b5d7e237d13befec17fb3e6b Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 4 Jun 2022 11:01:10 -0300 Subject: [PATCH 563/605] fix: hide .index folder on Windows --- launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index cbe16567..a3fcd9d9 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -22,6 +22,10 @@ #include "FileSystem.h" #include "minecraft/mod/MetadataHandler.h" +#ifdef Q_OS_WIN32 +#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) { @@ -29,6 +33,10 @@ LocalModUpdateTask::LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack& if (!FS::ensureFolderPathExists(index_dir.path())) { emitFailed(QString("Unable to create index for mod %1!").arg(m_mod.name)); } + +#ifdef Q_OS_WIN32 + SetFileAttributesA(index_dir.path().toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); +#endif } void LocalModUpdateTask::executeTask() From 5930acc41882222e746044b143aae99bd81c4afa Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Sat, 4 Jun 2022 22:54:05 +0800 Subject: [PATCH 564/605] change UI to scroll let me just say, this does not look right --- launcher/ui/pages/global/APIPage.ui | 462 +++++++++++++++------------- 1 file changed, 240 insertions(+), 222 deletions(-) diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 0981c700..eb8825b9 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -7,7 +7,7 @@ 0 0 800 - 702 + 712 @@ -34,230 +34,248 @@ - - - &Pastebin Service + + + QFrame::NoFrame - - - - - Paste Service &Type - - - pasteTypeComboBox - - - - - - - - - - Base &URL - - - baseURLEntry - - - - - - - - - - true - - - - - - - Note: you probably want to change or clear the Base URL after changing the paste service type. - - - true - - - - + + QFrame::Plain + + + Qt::ScrollBarAlwaysOff + + + false + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + 0 + -73 + 772 + 724 + + + + + + + &Pastebin Service + + + + + + Paste Service &Type + + + pasteTypeComboBox + + + + + + + + + + Base &URL + + + baseURLEntry + + + + + + + + + + true + + + + + + + Note: you probably want to change or clear the Base URL after changing the paste service type. + + + true + + + + + + + + + + &Microsoft Authentication + + + + + + Note: you probably don't need to set this if logging in via Microsoft Authentication already works. + + + Qt::RichText + + + true + + + + + + + (Default) + + + + + + + Enter a custom client ID for Microsoft Authentication here. + + + Qt::RichText + + + true + + + true + + + + + + + + + + Meta&data Server + + + + + + You can set this to a third-party metadata server to use patched libraries or other hacks. + + + Qt::RichText + + + true + + + + + + + + + + + + + + Enter a custom URL for meta here. + + + Qt::RichText + + + true + + + true + + + + + + + + + + true + + + &CurseForge Core API + + + + + + Note: you probably don't need to set this if CurseForge already works. + + + + + + + true + + + (Default) + + + + + + + Enter a custom API Key for CurseForge here. + + + Qt::RichText + + + true + + + true + + + + + + + + + + + 0 + 0 + + + + User Agent + + + + + + + + + Enter a custom User Agent here. The special string ${launcher_version} will be replaced with the version of the launcher. + + + + + + + + - - - - &Microsoft Authentication - - - - - - Note: you probably don't need to set this if logging in via Microsoft Authentication already works. - - - Qt::RichText - - - true - - - - - - - (Default) - - - - - - - Enter a custom client ID for Microsoft Authentication here. - - - Qt::RichText - - - true - - - true - - - - - - - - - - Meta&data Server - - - - - - You can set this to a third-party metadata server to use patched libraries or other hacks. - - - Qt::RichText - - - true - - - - - - - - - - - - - - Enter a custom URL for meta here. - - - Qt::RichText - - - true - - - true - - - - - - - - - - true - - - &CurseForge Core API - - - - - - Note: you probably don't need to set this if CurseForge already works. - - - - - - - true - - - (Default) - - - - - - - Enter a custom API Key for CurseForge here. - - - Qt::RichText - - - true - - - true - - - - - - - - - - - 0 - 0 - - - - User Agent - - - - - - - - - Enter a custom User Agent here. The special string ${launcher_version} will be replaced with the version of the launcher. - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - From 4cecba8787d3c4e9e8d1a0234a1850747b501a2e Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Sat, 4 Jun 2022 22:59:57 +0800 Subject: [PATCH 565/605] make $LAUNCHER_VER actually work --- launcher/Application.cpp | 12 ++++++------ launcher/ui/pages/global/APIPage.ui | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index dd6f8ec6..7143b767 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1557,9 +1557,9 @@ QString Application::getCurseKey() QString Application::getUserAgent() { - QString keyOverride = m_settings->get("UserAgentOverride").toString(); - if (!keyOverride.isEmpty()) { - return keyOverride; + QString uaOverride = m_settings->get("UserAgentOverride").toString(); + if (!uaOverride.isEmpty()) { + return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString()); } return BuildConfig.USER_AGENT; @@ -1567,9 +1567,9 @@ QString Application::getUserAgent() QString Application::getUserAgentUncached() { - QString keyOverride = m_settings->get("UserAgentOverride").toString(); - if (!keyOverride.isEmpty()) { - return keyOverride; + QString uaOverride = m_settings->get("UserAgentOverride").toString(); + if (!uaOverride.isEmpty()) { + return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString()); } return BuildConfig.USER_AGENT_UNCACHED; diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index eb8825b9..9524424e 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -265,7 +265,7 @@ - Enter a custom User Agent here. The special string ${launcher_version} will be replaced with the version of the launcher. + Enter a custom User Agent here. The special string $LAUNCHER_VER will be replaced with the version of the launcher. From 91b85f99190621baf4da28b6c9050becb5767041 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sat, 4 Jun 2022 17:09:11 +0200 Subject: [PATCH 566/605] Revert "Merge pull request #315 from txtsd/display_scaling" This reverts commit fcf728f3b5f4923cc05edfeb45f8340f420669cf. --- launcher/main.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/launcher/main.cpp b/launcher/main.cpp index 85c5fdee..3d25b4ff 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -27,10 +27,6 @@ int main(int argc, char *argv[]) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) - QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); -#endif - // initialize Qt Application app(argc, argv); From fcd56dddc2de6c7f5056f3ceb8166f4a2705dae7 Mon Sep 17 00:00:00 2001 From: RaptaG <77157639+RaptaG@users.noreply.github.com> Date: Sat, 4 Jun 2022 22:08:35 +0300 Subject: [PATCH 567/605] Capitalization fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a5cc154f..3dbc19c1 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ If there are any issues with the space or you are using a client that does not s [![Support](https://img.shields.io/matrix/polymc-support:matrix.org?label=PolyMC%20Support)](https://matrix.to/#/#polymc-support:matrix.org) [![Voice](https://img.shields.io/matrix/polymc-voice:matrix.org?label=PolyMC%20Voice)](https://matrix.to/#/#polymc-voice:matrix.org) -we also have a subreddit you can post your issues and suggestions on: +We also have a subreddit you can post your issues and suggestions on: [r/PolyMCLauncher](https://www.reddit.com/r/PolyMCLauncher/) From 7a3acc324979704e69a815bfe307aa054d4db8a3 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 4 Jun 2022 22:04:30 +0200 Subject: [PATCH 568/605] refactor(ui): use tabs for APIPage --- launcher/ui/pages/global/APIPage.cpp | 1 - launcher/ui/pages/global/APIPage.ui | 504 ++++++++++++++------------- 2 files changed, 263 insertions(+), 242 deletions(-) diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 0c1d7ca2..b889e6f7 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -75,7 +75,6 @@ APIPage::APIPage(QWidget *parent) : // This function needs to be called even when the ComboBox's index is still in its default state. updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry)); - ui->tabWidget->tabBar()->hide(); ui->metaURL->setPlaceholderText(BuildConfig.META_URL); ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT); diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 9524424e..5327771c 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -7,7 +7,7 @@ 0 0 800 - 712 + 600 @@ -30,252 +30,274 @@ - Tab 1 + Services - - - QFrame::NoFrame + + + &Pastebin Service - - QFrame::Plain - - - Qt::ScrollBarAlwaysOff - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - 0 - -73 - 772 - 724 - - - - - - - &Pastebin Service - - - - - - Paste Service &Type - - - pasteTypeComboBox - - - - - - - - - - Base &URL - - - baseURLEntry - - - - - - - - - - true - - - - - - - Note: you probably want to change or clear the Base URL after changing the paste service type. - - - true - - - - - - - - - - &Microsoft Authentication - - - - - - Note: you probably don't need to set this if logging in via Microsoft Authentication already works. - - - Qt::RichText - - - true - - - - - - - (Default) - - - - - - - Enter a custom client ID for Microsoft Authentication here. - - - Qt::RichText - - - true - - - true - - - - - - - - - - Meta&data Server - - - - - - You can set this to a third-party metadata server to use patched libraries or other hacks. - - - Qt::RichText - - - true - - - - - - - - - - - - - - Enter a custom URL for meta here. - - - Qt::RichText - - - true - - - true - - - - - - - - - - true - - - &CurseForge Core API - - - - - - Note: you probably don't need to set this if CurseForge already works. - - - - - - - true - - - (Default) - - - - - - - Enter a custom API Key for CurseForge here. - - - Qt::RichText - - - true - - - true - - - - - - - - - - - 0 - 0 - - - - User Agent - - - - - - - - - Enter a custom User Agent here. The special string $LAUNCHER_VER will be replaced with the version of the launcher. - - - - - - - - + + + + + Paste Service &Type + + + pasteTypeComboBox + + + + + + + + + + Base &URL + + + baseURLEntry + + + + + + + + + + true + + + + + + + Note: you probably want to change or clear the Base URL after changing the paste service type. + + + true + + + + + + + + Meta&data Server + + + + + + You can set this to a third-party metadata server to use patched libraries or other hacks. + + + Qt::RichText + + + true + + + + + + + + + + + + + + Enter a custom URL for meta here. + + + Qt::RichText + + + true + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + API Keys + + + + + + &Microsoft Authentication + + + + + + Note: you probably don't need to set this if logging in via Microsoft Authentication already works. + + + Qt::RichText + + + true + + + + + + + (Default) + + + + + + + Enter a custom client ID for Microsoft Authentication here. + + + Qt::RichText + + + true + + + true + + + + + + + + + + true + + + &CurseForge Core API + + + + + + Note: you probably don't need to set this if CurseForge already works. + + + + + + + Enter a custom API Key for CurseForge here. + + + Qt::RichText + + + true + + + true + + + + + + + true + + + (Default) + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Miscellaneous + + + + + + + 0 + 0 + + + + User Agent + + + + + + + + + Enter a custom User Agent here. The special string $LAUNCHER_VER will be replaced with the version of the launcher. + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + From cd49406bfec2d019cd9533f7a020107e551e7d61 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Sun, 5 Jun 2022 01:18:59 +0100 Subject: [PATCH 569/605] Fix launching process for some legacy Forge versions --- .../launcher/net/minecraft/Launcher.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/libraries/launcher/net/minecraft/Launcher.java b/libraries/launcher/net/minecraft/Launcher.java index 265fa66a..0b4d1c5c 100644 --- a/libraries/launcher/net/minecraft/Launcher.java +++ b/libraries/launcher/net/minecraft/Launcher.java @@ -28,11 +28,15 @@ public final class Launcher extends Applet implements AppletStub { private final Map params = new TreeMap<>(); - private final Applet wrappedApplet; + private Applet wrappedApplet; private boolean active = false; public Launcher(Applet applet) { + this(applet, null); + } + + public Launcher(Applet applet, URL documentBase) { this.setLayout(new BorderLayout()); this.add(applet, "Center"); @@ -40,8 +44,25 @@ public final class Launcher extends Applet implements AppletStub { this.wrappedApplet = applet; } - public void setParameter(String name, String value) - { + public void replace(Applet applet) { + this.wrappedApplet = applet; + + applet.setStub(this); + applet.setSize(getWidth(), getHeight()); + + this.setLayout(new BorderLayout()); + this.add(applet, "Center"); + + applet.init(); + + active = true; + + applet.start(); + + validate(); + } + + public void setParameter(String name, String value) { params.put(name, value); } From dd6d8e000238bdf9fad76cbebb787bf70546201d Mon Sep 17 00:00:00 2001 From: icelimetea Date: Sun, 5 Jun 2022 02:43:14 +0100 Subject: [PATCH 570/605] Make Launcher class to look more like original --- .../launcher/net/minecraft/Launcher.java | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/libraries/launcher/net/minecraft/Launcher.java b/libraries/launcher/net/minecraft/Launcher.java index 0b4d1c5c..6bf671be 100644 --- a/libraries/launcher/net/minecraft/Launcher.java +++ b/libraries/launcher/net/minecraft/Launcher.java @@ -24,12 +24,18 @@ import java.net.URL; import java.util.Map; import java.util.TreeMap; +/* + * WARNING: This class is reflectively accessed by legacy Forge versions. + * Changing field and method declarations without further testing is not recommended. + */ public final class Launcher extends Applet implements AppletStub { private final Map params = new TreeMap<>(); private Applet wrappedApplet; + private URL documentBase; + private boolean active = false; public Launcher(Applet applet) { @@ -42,6 +48,20 @@ public final class Launcher extends Applet implements AppletStub { this.add(applet, "Center"); this.wrappedApplet = applet; + + try { + if (documentBase != null) { + this.documentBase = documentBase; + } else if (applet.getClass().getPackage().getName().startsWith("com.mojang")) { + // Special case only for Classic versions + + this.documentBase = new URL("http", "www.minecraft.net", 80, "/game/"); + } else { + this.documentBase = new URL("http://www.minecraft.net/game/"); + } + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } } public void replace(Applet applet) { @@ -75,7 +95,7 @@ public final class Launcher extends Applet implements AppletStub { try { return super.getParameter(name); - } catch (Exception ignore) {} + } catch (Exception ignored) {} return null; } @@ -129,25 +149,13 @@ public final class Launcher extends Applet implements AppletStub { try { return new URL("http://www.minecraft.net/game/"); } catch (MalformedURLException e) { - e.printStackTrace(); + throw new RuntimeException(e); } - - return null; } @Override public URL getDocumentBase() { - try { - // Special case only for Classic versions - if (wrappedApplet.getClass().getCanonicalName().startsWith("com.mojang")) - return new URL("http", "www.minecraft.net", 80, "/game/"); - - return new URL("http://www.minecraft.net/game/"); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - - return null; + return documentBase; } @Override From 757fa1410cb6d065b2c26092b47dbe61f8c6d480 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Sun, 5 Jun 2022 23:52:21 +0800 Subject: [PATCH 571/605] Update launcher/Application.cpp Co-authored-by: Sefa Eyeoglu --- launcher/Application.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 7143b767..542b4d14 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1569,6 +1569,7 @@ QString Application::getUserAgentUncached() { QString uaOverride = m_settings->get("UserAgentOverride").toString(); if (!uaOverride.isEmpty()) { + uaOverride += " (Uncached)"; return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString()); } From 1c60e9b4fcaeae505232aa6287d76a2567d6ea1d Mon Sep 17 00:00:00 2001 From: MrMelon Date: Mon, 6 Jun 2022 18:12:50 +0100 Subject: [PATCH 572/605] Add initial sorting function --- launcher/icons/IconList.cpp | 28 ++++++++++++++++++++++++++++ launcher/icons/IconList.h | 1 + 2 files changed, 29 insertions(+) diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index 0ddfae55..e0debcb0 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -56,6 +56,32 @@ IconList::IconList(const QStringList &builtinPaths, QString path, QObject *paren emit iconUpdated({}); } +void IconList::sortIconList() +{ + qDebug() << "Sorting icon list..."; + + QVector newIcons = QVector(); + QVectorIterator iconIter(icons); + +iconLoop: + while(iconIter.hasNext()) + { + MMCIcon a = iconIter.next(); + for(int i=0;i Date: Mon, 6 Jun 2022 22:18:19 +0100 Subject: [PATCH 573/605] Simplify sorting logic to a single std::sort call --- launcher/icons/IconList.cpp | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index e0debcb0..522b39a7 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -59,26 +59,9 @@ IconList::IconList(const QStringList &builtinPaths, QString path, QObject *paren void IconList::sortIconList() { qDebug() << "Sorting icon list..."; - - QVector newIcons = QVector(); - QVectorIterator iconIter(icons); - -iconLoop: - while(iconIter.hasNext()) - { - MMCIcon a = iconIter.next(); - for(int i=0;i Date: Mon, 6 Jun 2022 22:13:10 -0400 Subject: [PATCH 574/605] nix: add package argument for extra jdks --- nix/default.nix | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nix/default.nix b/nix/default.nix index 969b455e..d6aa370c 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -14,6 +14,7 @@ , quazip , libGL , msaClientID ? "" +, extraJDKs ? [ ] # flake , self @@ -36,6 +37,8 @@ let # This variable will be passed to Minecraft by PolyMC gameLibraryPath = libpath + ":/run/opengl-driver/lib"; + + javaPaths = lib.makeSearchPath "bin/java" ([ jdk jdk8 ] ++ extraJDKs); in stdenv.mkDerivation rec { @@ -67,7 +70,7 @@ stdenv.mkDerivation rec { # xorg.xrandr needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 wrapQtApp $out/bin/polymc \ --set GAME_LIBRARY_PATH ${gameLibraryPath} \ - --prefix POLYMC_JAVA_PATHS : ${jdk}/lib/openjdk/bin/java:${jdk8}/lib/openjdk/bin/java \ + --prefix POLYMC_JAVA_PATHS : ${javaPaths} \ --prefix PATH : ${lib.makeBinPath [ xorg.xrandr ]} ''; From cd0d8a76c5aa3db14f4947fc16125569cab64c65 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 6 Jun 2022 21:00:10 +0200 Subject: [PATCH 575/605] chore: add sponsors to README --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 3dbc19c1..1e4e5caf 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,16 @@ If you do not agree with these terms and conditions, then remove the associated All launcher code is available under the GPL-3.0-only license. The logo and related assets are under the CC BY-SA 4.0 license. + +## Sponsors +Thank you to all our generous backers over at Open Collective! Support PolyMC by [becoming a backer](https://opencollective.com/polymc). + +[![OpenCollective Backers](https://opencollective.com/polymc/backers.svg?width=890&limit=1000)](https://opencollective.com/polymc#backers) + +Also, thanks to JetBrains for providing us a few licenses for all their products, as part of their [Open Source program](https://www.jetbrains.com/opensource/). + +[![JetBrains](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/opensource/) + +Additionally, thanks to the awesome people over at [MacStadium](https://www.macstadium.com/), for providing M1-Macs for development purposes! + +Powered by MacStadium From 1d9797660b70f6bd4026403a738c1d08fd3bba5d Mon Sep 17 00:00:00 2001 From: MrMelon Date: Tue, 7 Jun 2022 15:27:57 +0100 Subject: [PATCH 576/605] QString::locateAwareCompare() is better for human-like sorting --- launcher/icons/IconList.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index 522b39a7..d426aa80 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -60,7 +60,7 @@ void IconList::sortIconList() { qDebug() << "Sorting icon list..."; std::sort(icons.begin(), icons.end(), [](const MMCIcon& a, const MMCIcon& b) { - return a.m_key.compare(b.m_key) < 0; + return a.m_key.localeAwareCompare(b.m_key) < 0; }); reindex(); } From 6ee5ee496ed6eb2a62efc6d76b94d2613ef054c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20=C3=87al=C4=B1=C5=9Fkan?= Date: Wed, 8 Jun 2022 14:32:08 +0300 Subject: [PATCH 577/605] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:nixos/nixpkgs/41cc1d5d9584103be4108c1815c350e07c807036' (2022-05-23) → 'github:nixos/nixpkgs/43ecbe7840d155fa933ee8a500fb00dbbc651fc8' (2022-06-08) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index ccdd51da..120b11c5 100644 --- a/flake.lock +++ b/flake.lock @@ -34,11 +34,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1653326962, - "narHash": "sha256-W8feCYqKTsMre4nAEpv5Kx1PVFC+hao/LwqtB2Wci/8=", + "lastModified": 1654665288, + "narHash": "sha256-7blJpfoZEu7GKb84uh3io/5eSJNdaagXD9d15P9iQMs=", "owner": "nixos", "repo": "nixpkgs", - "rev": "41cc1d5d9584103be4108c1815c350e07c807036", + "rev": "43ecbe7840d155fa933ee8a500fb00dbbc651fc8", "type": "github" }, "original": { From 0c8ca1b3c0a64a2aae8b751cef64ae1071e046e0 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 8 Jun 2022 21:04:27 +0200 Subject: [PATCH 578/605] fix: remove debug CXX flags --- CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 11d58213..a0ae0a4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,8 +45,7 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00") # set CXXFLAGS for build targets -set(CMAKE_CXX_FLAGS_DEBUG "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") -set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") option(ENABLE_LTO "Enable Link Time Optimization" off) From 1b1f728c589a51db77eb711b4840307b897e3e67 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 9 Jun 2022 18:46:19 -0300 Subject: [PATCH 579/605] fix: allow opening external links in technic modpack page --- launcher/ui/pages/modplatform/technic/TechnicPage.ui | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index ca6a9b7e..15bf645f 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -60,7 +60,11 @@ - + + + true + + From 46e403b20b8f14269aebc163f2bc481d3dea43c5 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 9 Jun 2022 19:53:29 -0300 Subject: [PATCH 580/605] fix: properly parse mrpacks without the 'env' field It's optional, so some files may not have it (like most of FO). --- launcher/InstanceImportTask.cpp | 35 +++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 09c2a333..73f05d44 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -582,6 +582,7 @@ void InstanceImportTask::processMultiMC() emitSucceeded(); } +// https://docs.modrinth.com/docs/modpacks/format_definition/ void InstanceImportTask::processModrinth() { std::vector files; @@ -600,26 +601,30 @@ void InstanceImportTask::processModrinth() auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); bool had_optional = false; - for (auto& modInfo : jsonFiles) { + for (auto modInfo : jsonFiles) { Modrinth::File file; file.path = Json::requireString(modInfo, "path"); auto env = Json::ensureObject(modInfo, "env"); - QString support = Json::ensureString(env, "client", "unsupported"); - if (support == "unsupported") { - continue; - } else if (support == "optional") { - // TODO: Make a review dialog for choosing which ones the user wants! - if (!had_optional) { - had_optional = true; - auto info = CustomMessageBox::selectable( - m_parent, tr("Optional mod detected!"), - tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"), QMessageBox::Information); - info->exec(); - } + // 'env' field is optional + if (!env.isEmpty()) { + QString support = Json::ensureString(env, "client", "unsupported"); + if (support == "unsupported") { + continue; + } else if (support == "optional") { + // TODO: Make a review dialog for choosing which ones the user wants! + if (!had_optional) { + had_optional = true; + auto info = CustomMessageBox::selectable( + m_parent, tr("Optional mod detected!"), + tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"), + QMessageBox::Information); + info->exec(); + } - if (file.path.endsWith(".jar")) - file.path += ".disabled"; + if (file.path.endsWith(".jar")) + file.path += ".disabled"; + } } QJsonObject hashes = Json::requireObject(modInfo, "hashes"); From 1b878030aaba832ab416786c5f0dbc69da0e2166 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 9 Jun 2022 19:54:50 -0300 Subject: [PATCH 581/605] fix: enable using more than one download url in mrpacks Kinda, it's ugly and hackish, since we don't have the facilities to do this properly (yet!) --- launcher/InstanceImportTask.cpp | 52 ++++++++++++++----- .../modrinth/ModrinthPackManifest.h | 7 ++- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 73f05d44..74991e36 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -645,18 +645,32 @@ void InstanceImportTask::processModrinth() } file.hash = QByteArray::fromHex(hash.toLatin1()); file.hashAlgorithm = hashAlgorithm; + // 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"); + for(auto download : download_arr) { + qWarning() << download.toString(); + bool is_last = download.toString() == download_arr.last().toString(); - file.download = Json::requireString(Json::ensureArray(modInfo, "downloads").first(), "Download URL for " + file.path); + auto download_url = QUrl(download.toString()); - if (!file.download.isValid()) { - qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(file.download.toString(), file.path); - throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); - } - else if (!Modrinth::validateDownloadUrl(file.download)) { - qDebug() << QString("Download URL (%1) for %2 is from a non-whitelisted by Modrinth domain").arg(file.download.toString(), file.path); - non_whitelisted_files.push_back(file); + if (!download_url.isValid()) { + qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL") + .arg(download_url.toString(), file.path); + if(is_last && file.downloads.isEmpty()) + throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); + } + else { + if (!Modrinth::validateDownloadUrl(download_url)) { + qDebug() << QString("Download URL (%1) for %2 is from a non-whitelisted by Modrinth domain").arg(download_url.toString(), file.path); + if(is_last && file.downloads.isEmpty()) + non_whitelisted_files.push_back(file); + } + + file.downloads.push_back(download_url); + } } files.push_back(file); @@ -665,7 +679,10 @@ void InstanceImportTask::processModrinth() if (!non_whitelisted_files.empty()) { QString text; for (const auto& file : non_whitelisted_files) { - text += tr("Filepath: %1
URL: %2
").arg(file.path, file.download.toString()); + text += tr("Filepath: %1
").arg(file.path); + for(auto d : file.downloads) + text += tr("URL:") + QString("%2").arg(d.toString()); + text += "
"; } auto message_dialog = new ScrollMessageBox(m_parent, tr("Non-whitelisted mods found"), @@ -740,13 +757,24 @@ void InstanceImportTask::processModrinth() instance.saveNow(); m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); - for (auto &file : files) + for (auto file : files) { auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path); - qDebug() << "Will download" << file.download << "to" << path; - auto dl = Net::Download::makeFile(file.download, path); + qDebug() << "Will try to download" << file.downloads.front() << "to" << path; + auto dl = Net::Download::makeFile(file.downloads.front(), path); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); m_filesNetJob->addNetAction(dl); + + if (file.downloads.size() > 1) { + // FIXME: This really needs to be put into a ConcurrentTask of + // MultipleOptionsTask's , once those exist :) + connect(dl.get(), &NetAction::failed, [this, &file, path, dl]{ + auto dl = Net::Download::makeFile(file.downloads.dequeue(), path); + dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); + m_filesNetJob->addNetAction(dl); + dl->succeeded(); + }); + } } connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() { diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index e5fc9a70..b2083f57 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -40,6 +40,7 @@ #include #include +#include #include #include #include @@ -48,14 +49,12 @@ class MinecraftInstance; namespace Modrinth { -struct File -{ +struct File { QString path; QCryptographicHash::Algorithm hashAlgorithm; QByteArray hash; - // TODO: should this support multiple download URLs, like the JSON does? - QUrl download; + QQueue downloads; }; struct ModpackExtra { From b3c8f9d508b9110c13b3abf0f54d3f4927292559 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 9 Jun 2022 19:57:51 -0300 Subject: [PATCH 582/605] revert: don't check modrinth whitelisted hosts people didn't seem to like it, and its not required --- launcher/InstanceImportTask.cpp | 27 ------------------- .../modrinth/ModrinthPackManifest.cpp | 16 ----------- 2 files changed, 43 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 74991e36..1ccf7ffc 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -586,7 +586,6 @@ void InstanceImportTask::processMultiMC() void InstanceImportTask::processModrinth() { std::vector files; - std::vector non_whitelisted_files; QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion; try { QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); @@ -663,12 +662,6 @@ void InstanceImportTask::processModrinth() throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); } else { - if (!Modrinth::validateDownloadUrl(download_url)) { - qDebug() << QString("Download URL (%1) for %2 is from a non-whitelisted by Modrinth domain").arg(download_url.toString(), file.path); - if(is_last && file.downloads.isEmpty()) - non_whitelisted_files.push_back(file); - } - file.downloads.push_back(download_url); } } @@ -676,26 +669,6 @@ void InstanceImportTask::processModrinth() files.push_back(file); } - if (!non_whitelisted_files.empty()) { - QString text; - for (const auto& file : non_whitelisted_files) { - text += tr("Filepath: %1
").arg(file.path); - for(auto d : file.downloads) - text += tr("URL:") + QString("%2").arg(d.toString()); - text += "
"; - } - - auto message_dialog = new ScrollMessageBox(m_parent, tr("Non-whitelisted mods found"), - tr("The following mods have URLs that are not whitelisted by Modrinth.\n" - "Proceed with caution!"), - text); - message_dialog->setModal(true); - if (message_dialog->exec() == QDialog::Rejected) { - emitFailed("Aborted"); - return; - } - } - auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { QString name = it.key(); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 33116231..cc12f62f 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -95,19 +95,6 @@ void loadIndexedVersions(Modpack& pack, QJsonDocument& doc) pack.versionsLoaded = true; } -auto validateDownloadUrl(QUrl url) -> bool -{ - static QSet domainWhitelist{ - "cdn.modrinth.com", - "github.com", - "raw.githubusercontent.com", - "gitlab.com" - }; - - auto domain = url.host(); - return domainWhitelist.contains(domain); -} - auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion { ModpackVersion file; @@ -137,9 +124,6 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion auto url = Json::requireString(parent, "url"); - if(!validateDownloadUrl(url)) - continue; - file.download_url = url; if(is_primary) break; From 4a261cac1a71b0817ed9693da3b16796ada8f348 Mon Sep 17 00:00:00 2001 From: Vance <40771709+vancez@users.noreply.github.com> Date: Fri, 10 Jun 2022 10:25:13 +0800 Subject: [PATCH 583/605] fix: update toolbar when instance state changes --- launcher/ui/MainWindow.cpp | 11 +++++++++++ launcher/ui/MainWindow.h | 2 ++ 2 files changed, 13 insertions(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 7e152b96..c5a4cafe 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -2101,6 +2101,9 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & selectionBad(); return; } + if (m_selectedInstance) { + disconnect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::on_InstanceState_changed); + } QString id = current.data(InstanceList::InstanceIDRole).toString(); m_selectedInstance = APPLICATION->instances()->getInstanceById(id); if (m_selectedInstance) @@ -2127,6 +2130,8 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & updateToolsMenu(); APPLICATION->settings()->set("SelectedInstance", m_selectedInstance->id()); + + connect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::on_InstanceState_changed); } else { @@ -2216,3 +2221,9 @@ void MainWindow::updateStatusCenter() m_statusCenter->setText(tr("Total playtime: %1").arg(Time::prettifyDuration(timePlayed))); } } + +void MainWindow::on_InstanceState_changed(bool running) +{ + auto current = view->selectionModel()->currentIndex(); + instanceChanged(current, current); +} diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 2032acba..bd16246b 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -192,6 +192,8 @@ private slots: void keyReleaseEvent(QKeyEvent *event) override; #endif + void on_InstanceState_changed(bool running); + private: void retranslateUi(); From 529fb07b4200b5dada2a8eec2953b29fc535ec7d Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Fri, 10 Jun 2022 15:18:47 +0800 Subject: [PATCH 584/605] I changed my mind --- launcher/Application.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ff0f2129..29088c39 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1306,10 +1306,6 @@ void Application::subRunningInstance() bool Application::shouldExitNow() const { -#ifdef Q_OS_MACOS - return false; -#endif - return m_runningInstances == 0 && m_openWindows == 0; } From fa5b1d99786567a6a183513df6fdf63d1e667bc5 Mon Sep 17 00:00:00 2001 From: Vance <40771709+vancez@users.noreply.github.com> Date: Fri, 10 Jun 2022 15:48:18 +0800 Subject: [PATCH 585/605] change slot name --- launcher/ui/MainWindow.cpp | 6 +++--- launcher/ui/MainWindow.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index c5a4cafe..0a5f2000 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -2102,7 +2102,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & return; } if (m_selectedInstance) { - disconnect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::on_InstanceState_changed); + disconnect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); } QString id = current.data(InstanceList::InstanceIDRole).toString(); m_selectedInstance = APPLICATION->instances()->getInstanceById(id); @@ -2131,7 +2131,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & APPLICATION->settings()->set("SelectedInstance", m_selectedInstance->id()); - connect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::on_InstanceState_changed); + connect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); } else { @@ -2222,7 +2222,7 @@ void MainWindow::updateStatusCenter() } } -void MainWindow::on_InstanceState_changed(bool running) +void MainWindow::refreshCurrentInstance(bool running) { auto current = view->selectionModel()->currentIndex(); instanceChanged(current, current); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index bd16246b..61a75c45 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -192,7 +192,7 @@ private slots: void keyReleaseEvent(QKeyEvent *event) override; #endif - void on_InstanceState_changed(bool running); + void refreshCurrentInstance(bool running); private: void retranslateUi(); From 2ea20a8b29808308ce4b23b223457b3ebfb55174 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 11 Jun 2022 07:12:59 -0300 Subject: [PATCH 586/605] fix: allow discovering mrpacks in languages without dot --- launcher/ui/pages/modplatform/ImportPage.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index b3ed1b73..2ad7881d 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -114,7 +114,7 @@ void ImportPage::updateState() // Allow non-latin people to use ZIP files! auto zip = QMimeDatabase().mimeTypeForUrl(url).suffixes().contains("zip"); - if(fi.exists() && (zip || fi.suffix() == "mrpack")) + if(fi.exists() && (zip || fi.suffix() == "mrpack" || fi.fileName().endsWith("mrpack"))) { QFileInfo fi(url.fileName()); dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this)); @@ -149,7 +149,7 @@ void ImportPage::setUrl(const QString& url) void ImportPage::on_modpackBtn_clicked() { auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); - filter += ";;" + tr("Modrinth pack (*.mrpack)"); + filter += ";;" + tr("Modrinth pack (*.mrpack *mrpack)"); const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); if (url.isValid()) { From 81daffe68e5f91e94a9850ee98fc6ad45d911e5b Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 11 Jun 2022 13:57:40 +0200 Subject: [PATCH 587/605] fix: remove file filter from translation --- launcher/ui/pages/modplatform/ImportPage.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index 2ad7881d..0b8577b1 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -110,11 +110,13 @@ void ImportPage::updateState() { // FIXME: actually do some validation of what's inside here... this is fake AF QFileInfo fi(input); - // mrpack is a modrinth pack // Allow non-latin people to use ZIP files! - auto zip = QMimeDatabase().mimeTypeForUrl(url).suffixes().contains("zip"); - if(fi.exists() && (zip || fi.suffix() == "mrpack" || fi.fileName().endsWith("mrpack"))) + bool isZip = QMimeDatabase().mimeTypeForUrl(url).suffixes().contains("zip"); + // mrpack is a modrinth pack + bool isMRPack = fi.suffix() == "mrpack"; + + if(fi.exists() && (isZip || isMRPack)) { QFileInfo fi(url.fileName()); dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url,this)); @@ -149,7 +151,8 @@ void ImportPage::setUrl(const QString& url) void ImportPage::on_modpackBtn_clicked() { auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); - filter += ";;" + tr("Modrinth pack (*.mrpack *mrpack)"); + //: Option for filtering for *.mrpack files when importing + filter += ";;" + tr("Modrinth pack") + " (*.mrpack)"; const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); if (url.isValid()) { From 54144154f9761726edda4adf811e86b883f9603b Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 11 Jun 2022 13:43:09 -0300 Subject: [PATCH 588/605] fix: apply client overrides in mrpacks another oopsie x.x --- launcher/FileSystem.cpp | 43 +++++++++++++++++++++++++++++++++ launcher/FileSystem.h | 4 +++ launcher/InstanceImportTask.cpp | 18 +++++++++++--- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 6de20de6..3837d75f 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -454,4 +454,47 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na return false; #endif } + +QStringList listFolderPaths(QDir root) +{ + auto createAbsPath = [](QFileInfo const& entry) { return FS::PathCombine(entry.path(), entry.fileName()); }; + + QStringList entries; + + root.refresh(); + for (auto entry : root.entryInfoList(QDir::Filter::Files)) { + entries.append(createAbsPath(entry)); + } + + for (auto entry : root.entryInfoList(QDir::Filter::AllDirs | QDir::Filter::NoDotAndDotDot)) { + entries.append(listFolderPaths(createAbsPath(entry))); + } + + return entries; +} + +bool overrideFolder(QString overwritten_path, QString override_path) +{ + if (!FS::ensureFolderPathExists(overwritten_path)) + return false; + + QStringList paths_to_override; + QDir root_override (override_path); + for (auto file : listFolderPaths(root_override)) { + QString destination = file; + destination.replace(override_path, overwritten_path); + + qDebug() << QString("Applying override %1 in %2").arg(file, destination); + + if (QFile::exists(destination)) + QFile::remove(destination); + if (!QFile::rename(file, destination)) { + qCritical() << QString("Failed to apply override from %1 to %2").arg(file, destination); + return false; + } + } + + return true; +} + } diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index 8f6e8b48..bc942ab3 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -124,4 +124,8 @@ QString getDesktopDir(); // call it *name* and assign it the icon *icon* // return true if operation succeeded bool createShortCut(QString location, QString dest, QStringList args, QString name, QString iconLocation); + +// 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); } diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 1ccf7ffc..3dcd92c8 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -696,16 +696,26 @@ void InstanceImportTask::processModrinth() emitFailed(tr("Could not understand pack index:\n") + e.cause()); return; } + + auto mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); - QString overridePath = FS::PathCombine(m_stagingPath, "overrides"); - if (QFile::exists(overridePath)) { - QString mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); - if (!QFile::rename(overridePath, mcPath)) { + auto override_path = FS::PathCombine(m_stagingPath, "overrides"); + if (QFile::exists(override_path)) { + if (!QFile::rename(override_path, mcPath)) { emitFailed(tr("Could not rename the overrides folder:\n") + "overrides"); return; } } + // Do client overrides + auto client_override_path = FS::PathCombine(m_stagingPath, "client-overrides"); + if (QFile::exists(client_override_path)) { + if (!FS::overrideFolder(mcPath, client_override_path)) { + emitFailed(tr("Could not rename the client overrides folder:\n") + "client overrides"); + return; + } + } + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(configPath); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); From 29e5a213a5be2d3716018b64241ac030ca2b0af5 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 11 Jun 2022 14:19:51 -0300 Subject: [PATCH 589/605] fix: dequeue first added file in mrpack import Co-authored-by: Sefa Eyeoglu --- launcher/InstanceImportTask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 3dcd92c8..1498db6f 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -744,7 +744,7 @@ void InstanceImportTask::processModrinth() { auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path); qDebug() << "Will try to download" << file.downloads.front() << "to" << path; - auto dl = Net::Download::makeFile(file.downloads.front(), path); + auto dl = Net::Download::makeFile(file.downloads.dequeue(), path); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); m_filesNetJob->addNetAction(dl); From 37160f973f1d2fed450f40f38a55b8445787c4bd Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 11 Jun 2022 14:31:50 -0300 Subject: [PATCH 590/605] fix: account for the dequeued url when checking the number of urls Co-authored-by: Sefa Eyeoglu --- launcher/InstanceImportTask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 1498db6f..d5684805 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -748,7 +748,7 @@ void InstanceImportTask::processModrinth() dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); m_filesNetJob->addNetAction(dl); - if (file.downloads.size() > 1) { + if (file.downloads.size() > 0) { // FIXME: This really needs to be put into a ConcurrentTask of // MultipleOptionsTask's , once those exist :) connect(dl.get(), &NetAction::failed, [this, &file, path, dl]{ From 8683d529fc8182adc394a851bb9d1c4c68bf959e Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 11 Jun 2022 19:35:40 +0200 Subject: [PATCH 591/605] chore: bump version --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 11d58213..0fed9f18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,8 +73,8 @@ set(Launcher_HELP_URL "https://polymc.org/wiki/help-pages/%1" CACHE STRING "URL ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 1) -set(Launcher_VERSION_MINOR 3) -set(Launcher_VERSION_HOTFIX 1) +set(Launcher_VERSION_MINOR 4) +set(Launcher_VERSION_HOTFIX 0) # Build number set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") From 8a0aa5a0c852c3a8043d24831be30ccc89aa32d0 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 11 Jun 2022 23:06:42 +0200 Subject: [PATCH 592/605] fix: avoid re-registering InstanceType --- launcher/BaseInstance.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 0240afa8..f02205e9 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -59,7 +59,11 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("lastLaunchTime", 0); m_settings->registerSetting("totalTimePlayed", 0); m_settings->registerSetting("lastTimePlayed", 0); - m_settings->registerSetting("InstanceType", ""); + + // NOTE: Sometimees InstanceType is already registered, as it was used to identify the type of + // a locally stored instance + if (!m_settings->getSetting("InstanceType")) + m_settings->registerSetting("InstanceType", ""); // Custom Commands auto commandSetting = m_settings->registerSetting({"OverrideCommands","OverrideLaunchCmd"}, false); From 13b03e7e503dacdb7a3251a9804c520aae641db0 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Sun, 12 Jun 2022 11:44:04 +0800 Subject: [PATCH 593/605] Update Application.cpp --- launcher/Application.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 29088c39..dfa756d4 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -967,7 +967,6 @@ bool Application::event(QEvent* event) { if (m_prevAppState == Qt::ApplicationActive && ev->applicationState() == Qt::ApplicationActive) { - qDebug() << "Clicked on dock!"; emit clickedOnDock(); } m_prevAppState = ev->applicationState(); From e843b8e1884f9d0e5b94963d92df4e990fcf8e45 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 4 Jun 2022 08:56:03 -0300 Subject: [PATCH 594/605] fix(test): fix packwiz test --- launcher/modplatform/packwiz/Packwiz_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/launcher/modplatform/packwiz/Packwiz_test.cpp index 023b990e..f7a52e4a 100644 --- a/launcher/modplatform/packwiz/Packwiz_test.cpp +++ b/launcher/modplatform/packwiz/Packwiz_test.cpp @@ -61,7 +61,7 @@ class PackwizTest : public QObject { QVERIFY(index_dir.entryList().contains(name_mod)); // Try without the .pw.toml at the end - name_mod.chop(5); + name_mod.chop(8); auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod); From 8856c8cd62fe3f45faf1020e70fa3dc503eb3453 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 4 Jun 2022 15:30:34 +0200 Subject: [PATCH 595/605] refactor(test): fix loading mod metadata setting --- launcher/ModDownloadTask.cpp | 8 +++++--- launcher/ModDownloadTask.h | 2 +- launcher/minecraft/MinecraftInstance.cpp | 6 ++++-- launcher/minecraft/mod/Mod.cpp | 2 +- launcher/minecraft/mod/ModFolderModel.cpp | 4 ++-- launcher/minecraft/mod/ModFolderModel.h | 3 ++- launcher/minecraft/mod/ModFolderModel_test.cpp | 4 ++-- launcher/minecraft/mod/ResourcePackFolderModel.cpp | 2 +- launcher/minecraft/mod/TexturePackFolderModel.cpp | 2 +- launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp | 5 ----- launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp | 6 +++--- launcher/minecraft/mod/tasks/ModFolderLoadTask.h | 3 ++- launcher/ui/pages/global/LauncherPage.cpp | 2 +- launcher/ui/pages/modplatform/ModPage.cpp | 4 +++- 14 files changed, 28 insertions(+), 25 deletions(-) diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp index 301b6637..a54baec4 100644 --- a/launcher/ModDownloadTask.cpp +++ b/launcher/ModDownloadTask.cpp @@ -21,12 +21,14 @@ #include "Application.h" #include "minecraft/mod/ModFolderModel.h" -ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr mods) +ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr mods, bool is_indexed) : m_mod(mod), m_mod_version(version), mods(mods) { - m_update_task.reset(new LocalModUpdateTask(mods->indexDir(), m_mod, m_mod_version)); + if (is_indexed) { + m_update_task.reset(new LocalModUpdateTask(mods->indexDir(), m_mod, m_mod_version)); - addTask(m_update_task); + addTask(m_update_task); + } m_filesNetJob.reset(new NetJob(tr("Mod download"), APPLICATION->network())); m_filesNetJob->setStatus(tr("Downloading mod:\n%1").arg(m_mod_version.downloadUrl)); diff --git a/launcher/ModDownloadTask.h b/launcher/ModDownloadTask.h index f4438a8d..06a8a6de 100644 --- a/launcher/ModDownloadTask.h +++ b/launcher/ModDownloadTask.h @@ -29,7 +29,7 @@ class ModFolderModel; class ModDownloadTask : public SequentialTask { Q_OBJECT public: - explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr mods); + explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr mods, bool is_indexed); const QString& getFilename() const { return m_mod_version.fileName; } private: diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index e99d30fe..7e72601f 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1015,7 +1015,8 @@ std::shared_ptr MinecraftInstance::loaderModList() const { if (!m_loader_mod_list) { - m_loader_mod_list.reset(new ModFolderModel(modsRoot())); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_loader_mod_list.reset(new ModFolderModel(modsRoot(), is_indexed)); m_loader_mod_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction); } @@ -1026,7 +1027,8 @@ std::shared_ptr MinecraftInstance::coreModList() const { if (!m_core_mod_list) { - m_core_mod_list.reset(new ModFolderModel(coreModsDir())); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_core_mod_list.reset(new ModFolderModel(coreModsDir(), is_indexed)); m_core_mod_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction); } diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 71a32d32..ff855230 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -161,7 +161,7 @@ auto Mod::destroy(QDir& index_dir) -> bool { auto n = name(); // FIXME: This can fail to remove the metadata if the - // "DontUseModMetadata" setting is on, since there could + // "ModMetadataDisabled" setting is on, since there could // be a name mismatch! Metadata::remove(index_dir, n); diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index b2d8f03e..bb52bbe4 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -28,7 +28,7 @@ #include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" -ModFolderModel::ModFolderModel(const QString &dir) : QAbstractListModel(), m_dir(dir) +ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : QAbstractListModel(), m_dir(dir), m_is_indexed(is_indexed) { FS::ensureFolderPathExists(m_dir.absolutePath()); m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); @@ -82,7 +82,7 @@ bool ModFolderModel::update() } auto index_dir = indexDir(); - auto task = new ModFolderLoadTask(dir(), index_dir); + auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed); m_update = task->result(); diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 10a72691..efec2f3f 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -52,7 +52,7 @@ public: Enable, Toggle }; - ModFolderModel(const QString &dir); + ModFolderModel(const QString &dir, bool is_indexed); virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; @@ -146,6 +146,7 @@ protected: bool scheduled_update = false; bool interaction_disabled = false; QDir m_dir; + bool m_is_indexed; QMap modsIndex; QMap activeTickets; int nextResolutionTicket = 0; diff --git a/launcher/minecraft/mod/ModFolderModel_test.cpp b/launcher/minecraft/mod/ModFolderModel_test.cpp index 76f16ed5..429d82b3 100644 --- a/launcher/minecraft/mod/ModFolderModel_test.cpp +++ b/launcher/minecraft/mod/ModFolderModel_test.cpp @@ -32,7 +32,7 @@ slots: { QString folder = source; QTemporaryDir tempDir; - ModFolderModel m(tempDir.path()); + ModFolderModel m(tempDir.path(), true); m.installMod(folder); verify(tempDir.path()); } @@ -41,7 +41,7 @@ slots: { QString folder = source + '/'; QTemporaryDir tempDir; - ModFolderModel m(tempDir.path()); + ModFolderModel m(tempDir.path(), true); m.installMod(folder); verify(tempDir.path()); } diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index f3d7f566..35d179ee 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -1,6 +1,6 @@ #include "ResourcePackFolderModel.h" -ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) { +ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir, false) { } QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index d5956da1..96fea33e 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -1,6 +1,6 @@ #include "TexturePackFolderModel.h" -TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) { +TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir, false) { } QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index cbe16567..b8170003 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -35,11 +35,6 @@ void LocalModUpdateTask::executeTask() { setStatus(tr("Updating index for mod:\n%1").arg(m_mod.name)); - if(APPLICATION->settings()->get("DontUseModMetadata").toBool()){ - emitSucceeded(); - return; - } - auto pw_mod = Metadata::create(m_index_dir, m_mod, m_mod_version); Metadata::update(m_index_dir, pw_mod); diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 62d856f6..285225a0 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -38,13 +38,13 @@ #include "Application.h" #include "minecraft/mod/MetadataHandler.h" -ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir) - : m_mods_dir(mods_dir), m_index_dir(index_dir), m_result(new Result()) +ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed) + : m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_result(new Result()) {} void ModFolderLoadTask::run() { - if (!APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + if (m_is_indexed) { // Read metadata first getFromMetadata(); } diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h index 89a0f84e..bc162f43 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h @@ -55,7 +55,7 @@ public: } public: - ModFolderLoadTask(QDir& mods_dir, QDir& index_dir); + ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed); void run(); signals: void succeeded(); @@ -65,5 +65,6 @@ private: private: QDir& m_mods_dir, m_index_dir; + bool m_is_indexed; ResultPtr m_result; }; diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index faf9272d..4be24979 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -450,7 +450,7 @@ void LauncherPage::loadSettings() } // Mods - ui->metadataDisableBtn->setChecked(s->get("DontUseModMetadata").toBool()); + ui->metadataDisableBtn->setChecked(s->get("ModMetadataDisabled").toBool()); ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); } diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 5020d44c..e43a087c 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -1,4 +1,5 @@ #include "ModPage.h" +#include "Application.h" #include "ui_ModPage.h" #include @@ -150,7 +151,8 @@ void ModPage::onModSelected() if (dialog->isModSelected(current.name, version.fileName)) { dialog->removeSelectedMod(current.name); } else { - dialog->addSelectedMod(current.name, new ModDownloadTask(current, version, dialog->mods)); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + dialog->addSelectedMod(current.name, new ModDownloadTask(current, version, dialog->mods, is_indexed)); } updateSelectionButton(); From 32217a774f9902d3d523e7b7985bbe22060d0451 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 12 Jun 2022 12:39:04 +0200 Subject: [PATCH 596/605] fix(tests): wait until ModFolderModel has updated --- launcher/minecraft/mod/ModFolderModel_test.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/launcher/minecraft/mod/ModFolderModel_test.cpp b/launcher/minecraft/mod/ModFolderModel_test.cpp index 429d82b3..21e905f4 100644 --- a/launcher/minecraft/mod/ModFolderModel_test.cpp +++ b/launcher/minecraft/mod/ModFolderModel_test.cpp @@ -32,8 +32,11 @@ slots: { QString folder = source; QTemporaryDir tempDir; + QEventLoop loop; ModFolderModel m(tempDir.path(), true); + connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); m.installMod(folder); + loop.exec(); verify(tempDir.path()); } @@ -41,8 +44,11 @@ slots: { QString folder = source + '/'; QTemporaryDir tempDir; + QEventLoop loop; ModFolderModel m(tempDir.path(), true); + connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); m.installMod(folder); + loop.exec(); verify(tempDir.path()); } } From 2ff0aa09e35eb6910ef0a030ea41f84a1ed95782 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 12 Jun 2022 12:22:48 +0200 Subject: [PATCH 597/605] fix: remove updater if it is not used --- launcher/Application.cpp | 6 +++ launcher/Application.h | 3 ++ launcher/CMakeLists.txt | 38 ++++++++++--------- .../minecraft/launch/LauncherPartLaunch.cpp | 1 + launcher/net/PasteUpload.cpp | 2 + launcher/ui/GuiUtil.cpp | 1 + launcher/ui/MainWindow.cpp | 10 +++++ launcher/ui/MainWindow.h | 8 ++++ launcher/ui/pages/global/LauncherPage.cpp | 9 +++-- launcher/ui/pages/global/LauncherPage.h | 5 ++- launcher/ui/pages/global/LauncherPage.ui | 3 ++ launcher/ui/pages/instance/LogPage.cpp | 1 + launcher/ui/pages/instance/ModFolderPage.h | 1 + launcher/ui/pages/instance/ScreenshotsPage.h | 1 + launcher/ui/pages/instance/ServersPage.cpp | 1 + launcher/ui/pages/instance/WorldListPage.cpp | 1 + .../modplatform/legacy_ftb/ListModel.cpp | 2 + .../modplatform/modrinth/ModrinthModel.h | 1 + 18 files changed, 70 insertions(+), 24 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 4e0393c0..ab3110a3 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -154,6 +154,7 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QSt fflush(stderr); } +#ifdef LAUNCHER_WITH_UPDATER QString getIdealPlatform(QString currentPlatform) { auto info = Sys::getKernelInfo(); switch(info.kernelType) { @@ -192,6 +193,7 @@ QString getIdealPlatform(QString currentPlatform) { } return currentPlatform; } +#endif } @@ -754,6 +756,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) qDebug() << "<> Translations loaded."; } +#ifdef LAUNCHER_WITH_UPDATER // initialize the updater if(BuildConfig.UPDATER_ENABLED) { @@ -763,6 +766,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_CHANNEL, BuildConfig.VERSION_BUILD)); qDebug() << "<> Updater started."; } +#endif // Instance icons { @@ -1408,7 +1412,9 @@ MainWindow* Application::showMainWindow(bool minimized) } m_mainWindow->checkInstancePathForProblems(); +#ifdef LAUNCHER_WITH_UPDATER connect(this, &Application::updateAllowedChanged, m_mainWindow, &MainWindow::updatesAllowedChanged); +#endif connect(m_mainWindow, &MainWindow::isClosing, this, &Application::on_windowClose); m_openWindows++; } diff --git a/launcher/Application.h b/launcher/Application.h index e08e354a..09007160 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -42,7 +42,10 @@ #include #include #include + +#ifdef LAUNCHER_WITH_UPDATER #include +#endif #include diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 5397a988..b8db803b 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -156,27 +156,29 @@ set(LAUNCH_SOURCES launch/LogModel.h ) -# Old update system -set(UPDATE_SOURCES - updater/GoUpdate.h - updater/GoUpdate.cpp - updater/UpdateChecker.h - updater/UpdateChecker.cpp - updater/DownloadTask.h - updater/DownloadTask.cpp -) - -add_unit_test(UpdateChecker - SOURCES updater/UpdateChecker_test.cpp - LIBS Launcher_logic - DATA updater/testdata +if (Launcher_UPDATER_BASE) + set(Launcher_APP_BINARY_DEFS "-DLAUNCHER_WITH_UPDATER ${Launcher_APP_BINARY_DEFS}") + # Old update system + set(UPDATE_SOURCES + updater/GoUpdate.h + updater/GoUpdate.cpp + updater/UpdateChecker.h + updater/UpdateChecker.cpp + updater/DownloadTask.h + updater/DownloadTask.cpp ) -add_unit_test(DownloadTask - SOURCES updater/DownloadTask_test.cpp - LIBS Launcher_logic - DATA updater/testdata + add_unit_test(UpdateChecker + SOURCES updater/UpdateChecker_test.cpp + LIBS Launcher_logic + DATA updater/testdata ) + add_unit_test(DownloadTask + SOURCES updater/DownloadTask_test.cpp + LIBS Launcher_logic + DATA updater/testdata + ) +endif() # Backend for the news bar... there's usually no news. set(NEWS_SOURCES diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index d7010355..d6fc11e7 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -16,6 +16,7 @@ #include "LauncherPartLaunch.h" #include +#include #include "launch/LaunchTask.h" #include "minecraft/MinecraftInstance.h" diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index ead5e170..2f200a99 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -43,6 +43,8 @@ #include #include #include +#include +#include std::array PasteUpload::PasteTypes = { {{"0x0.st", "https://0x0.st", ""}, diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 320f1502..62f36951 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/CustomMessageBox.h" diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 0a5f2000..a6168e7a 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1010,6 +1010,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow } +#ifdef LAUNCHER_WITH_UPDATER if(BuildConfig.UPDATER_ENABLED) { bool updatesAllowed = APPLICATION->updatesAreAllowed(); @@ -1028,6 +1029,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), false); } } +#endif setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString()); @@ -1337,6 +1339,7 @@ void MainWindow::repopulateAccountsMenu() ui->profileMenu->addAction(ui->actionManageAccounts); } +#ifdef LAUNCHER_WITH_UPDATER void MainWindow::updatesAllowedChanged(bool allowed) { if(!BuildConfig.UPDATER_ENABLED) @@ -1345,6 +1348,7 @@ void MainWindow::updatesAllowedChanged(bool allowed) } ui->actionCheckUpdate->setEnabled(allowed); } +#endif /* * Assumes the sender is a QAction @@ -1450,6 +1454,7 @@ void MainWindow::updateNewsLabel() } } +#ifdef LAUNCHER_WITH_UPDATER void MainWindow::updateAvailable(GoUpdate::Status status) { if(!APPLICATION->updatesAreAllowed()) @@ -1475,6 +1480,7 @@ void MainWindow::updateNotAvailable() UpdateDialog dlg(false, this); dlg.exec(); } +#endif QList stringToIntList(const QString &string) { @@ -1496,6 +1502,7 @@ QString intListToString(const QList &list) return slist.join(','); } +#ifdef LAUNCHER_WITH_UPDATER void MainWindow::downloadUpdates(GoUpdate::Status status) { if(!APPLICATION->updatesAreAllowed()) @@ -1529,6 +1536,7 @@ void MainWindow::downloadUpdates(GoUpdate::Status status) CustomMessageBox::selectable(this, tr("Error"), updateTask.failReason(), QMessageBox::Warning)->show(); } } +#endif void MainWindow::onCatToggled(bool state) { @@ -1841,6 +1849,7 @@ void MainWindow::on_actionConfig_Folder_triggered() } } +#ifdef LAUNCHER_WITH_UPDATER void MainWindow::checkForUpdates() { if(BuildConfig.UPDATER_ENABLED) @@ -1853,6 +1862,7 @@ void MainWindow::checkForUpdates() qWarning() << "Updater not set up. Cannot check for updates."; } } +#endif void MainWindow::on_actionSettings_triggered() { diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 61a75c45..abd4db92 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -54,7 +54,9 @@ public: void checkInstancePathForProblems(); +#ifdef LAUNCHER_WITH_UPDATER void updatesAllowedChanged(bool allowed); +#endif void droppedURLs(QList urls); signals: @@ -100,7 +102,9 @@ private slots: void on_actionViewCentralModsFolder_triggered(); +#ifdef LAUNCHER_WITH_UPDATER void checkForUpdates(); +#endif void on_actionSettings_triggered(); @@ -167,9 +171,11 @@ private slots: void startTask(Task *task); +#ifdef LAUNCHER_WITH_UPDATER void updateAvailable(GoUpdate::Status status); void updateNotAvailable(); +#endif void defaultAccountChanged(); @@ -179,10 +185,12 @@ private slots: void updateNewsLabel(); +#ifdef LAUNCHER_WITH_UPDATER /*! * Runs the DownloadTask and installs updates. */ void downloadUpdates(GoUpdate::Status status); +#endif void konamiTriggered(); diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 4be24979..edbf609f 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -78,6 +78,7 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch m_languageModel = APPLICATION->translations(); loadSettings(); +#ifdef LAUNCHER_WITH_UPDATER if(BuildConfig.UPDATER_ENABLED) { QObject::connect(APPLICATION->updateChecker().get(), &UpdateChecker::channelListLoaded, this, &LauncherPage::refreshUpdateChannelList); @@ -90,11 +91,9 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch { APPLICATION->updateChecker()->updateChanList(false); } + ui->updateSettingsBox->setHidden(false); } - else - { - ui->updateSettingsBox->setHidden(true); - } +#endif connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview())); connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview())); } @@ -189,6 +188,7 @@ void LauncherPage::on_metadataDisableBtn_clicked() ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); } +#ifdef LAUNCHER_WITH_UPDATER void LauncherPage::refreshUpdateChannelList() { // Stop listening for selection changes. It's going to change a lot while we update it and @@ -260,6 +260,7 @@ void LauncherPage::refreshUpdateChannelDesc() m_currentUpdateChannel = selected.id; } } +#endif void LauncherPage::applySettings() { diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index f38c922e..ccfd7e9e 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -90,6 +90,7 @@ slots: void on_iconsDirBrowseBtn_clicked(); void on_metadataDisableBtn_clicked(); +#ifdef LAUNCHER_WITH_UPDATER /*! * Updates the list of update channels in the combo box. */ @@ -100,13 +101,13 @@ slots: */ void refreshUpdateChannelDesc(); + void updateChannelSelectionChanged(int index); +#endif /*! * Updates the font preview */ void refreshFontPreview(); - void updateChannelSelectionChanged(int index); - private: Ui::LauncherPage *ui; diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 417bbe05..ceb68c5b 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -50,6 +50,9 @@ Update Settings + + false + diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 30a8735f..51303501 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -39,6 +39,7 @@ #include "Application.h" #include +#include #include #include diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 72e2d404..2dd44e85 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -41,6 +41,7 @@ #include "ui/pages/BasePage.h" #include +#include class ModFolderModel; namespace Ui diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index c22706af..c34c9755 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -35,6 +35,7 @@ #pragma once +#include #include #include "ui/pages/BasePage.h" diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 2af6164c..d6303cdd 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -47,6 +47,7 @@ #include #include +#include static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things. diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 76725539..731dd85f 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -46,6 +46,7 @@ #include #include #include +#include #include "tools/MCEditTool.h" #include "FileSystem.h" diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 63b944c4..0d8e1dcf 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -11,6 +11,8 @@ #include +#include + namespace LegacyFTB { FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 14aa6747..1b4d8da4 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -39,6 +39,7 @@ #include "modplatform/modrinth/ModrinthPackManifest.h" #include "ui/pages/modplatform/modrinth/ModrinthPage.h" +#include "net/NetJob.h" class ModPage; class Version; From a4ef0940ed76d646db1b1be1224da2baab4be9e2 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 12 Jun 2022 13:50:58 +0200 Subject: [PATCH 598/605] chore: add license headers --- launcher/ModDownloadTask.cpp | 1 + launcher/ModDownloadTask.h | 1 + .../minecraft/launch/LauncherPartLaunch.cpp | 40 +++++++++++---- launcher/minecraft/mod/Mod.cpp | 1 + launcher/minecraft/mod/ModFolderModel.cpp | 49 +++++++++++++------ launcher/minecraft/mod/ModFolderModel.h | 49 +++++++++++++------ .../minecraft/mod/ModFolderModel_test.cpp | 34 +++++++++++++ .../minecraft/mod/ResourcePackFolderModel.cpp | 35 +++++++++++++ .../minecraft/mod/TexturePackFolderModel.cpp | 35 +++++++++++++ .../mod/tasks/LocalModUpdateTask.cpp | 1 + .../minecraft/mod/tasks/ModFolderLoadTask.cpp | 1 + .../minecraft/mod/tasks/ModFolderLoadTask.h | 1 + launcher/modplatform/packwiz/Packwiz_test.cpp | 31 ++++++------ launcher/net/PasteUpload.cpp | 1 + launcher/ui/GuiUtil.cpp | 1 + launcher/ui/MainWindow.cpp | 47 +++++++++++++----- launcher/ui/MainWindow.h | 44 +++++++++++++---- launcher/ui/pages/global/LanguagePage.cpp | 1 + launcher/ui/pages/global/LanguagePage.h | 1 + launcher/ui/pages/instance/LogPage.cpp | 1 + launcher/ui/pages/instance/ModFolderPage.cpp | 1 + .../ui/pages/instance/ScreenshotsPage.cpp | 1 + launcher/ui/pages/instance/ServersPage.cpp | 1 + launcher/ui/pages/instance/WorldListPage.cpp | 1 + launcher/ui/pages/modplatform/ModPage.cpp | 35 +++++++++++++ .../modplatform/legacy_ftb/ListModel.cpp | 35 +++++++++++++ .../modplatform/modrinth/ModrinthModel.cpp | 1 + 27 files changed, 374 insertions(+), 76 deletions(-) diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp index a54baec4..41856fb5 100644 --- a/launcher/ModDownloadTask.cpp +++ b/launcher/ModDownloadTask.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 flowln +* Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ModDownloadTask.h b/launcher/ModDownloadTask.h index 06a8a6de..b3c25909 100644 --- a/launcher/ModDownloadTask.h +++ b/launcher/ModDownloadTask.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 flowln +* Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index d6fc11e7..427bc32b 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 "LauncherPartLaunch.h" diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index ff855230..a85aecfb 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 flowln +* Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index bb52bbe4..ded2d3a2 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -1,17 +1,38 @@ -/* 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. - */ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln +* Copyright (C) 2022 Sefa Eyeoglu +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 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 "ModFolderModel.h" diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index efec2f3f..fcedae96 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -1,17 +1,38 @@ -/* 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. - */ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln +* Copyright (C) 2022 Sefa Eyeoglu +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 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 diff --git a/launcher/minecraft/mod/ModFolderModel_test.cpp b/launcher/minecraft/mod/ModFolderModel_test.cpp index 21e905f4..34a3b83a 100644 --- a/launcher/minecraft/mod/ModFolderModel_test.cpp +++ b/launcher/minecraft/mod/ModFolderModel_test.cpp @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (C) 2022 Sefa Eyeoglu +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 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 diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index 35d179ee..fb1f1d29 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (C) 2022 Sefa Eyeoglu +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 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 "ResourcePackFolderModel.h" ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir, false) { diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 96fea33e..644dfd77 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (C) 2022 Sefa Eyeoglu +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* +* This file incorporates work covered by the following copyright and +* permission notice: +* +* Copyright 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 "TexturePackFolderModel.h" TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir, false) { diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index b8170003..018bc6e3 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 flowln +* Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 285225a0..af67d305 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 flowln +* Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h index bc162f43..088f873e 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 flowln +* Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/launcher/modplatform/packwiz/Packwiz_test.cpp index f7a52e4a..3d47f9f7 100644 --- a/launcher/modplatform/packwiz/Packwiz_test.cpp +++ b/launcher/modplatform/packwiz/Packwiz_test.cpp @@ -1,20 +1,21 @@ // SPDX-License-Identifier: GPL-3.0-only /* -* PolyMC - 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 . -*/ + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #include #include diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 2f200a99..7438e1a1 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -3,6 +3,7 @@ * PolyMC - Minecraft Launcher * Copyright (C) 2022 Lenny McLennington * Copyright (C) 2022 Swirl + * 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 diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 62f36951..b1ea5ee9 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Lenny McLennington + * 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 diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index a6168e7a..210442df 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1,21 +1,42 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * Authors: Andrew Okin - * Peterix - * Orochimarufan + * 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. * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . * - * 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. + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Authors: Andrew Okin + * Peterix + * Orochimarufan + * + * 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 "Application.h" #include "BuildConfig.h" diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index abd4db92..6c64756f 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -1,16 +1,40 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * 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 + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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 + * + * Authors: Andrew Okin + * Peterix + * Orochimarufan + * + * 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 diff --git a/launcher/ui/pages/global/LanguagePage.cpp b/launcher/ui/pages/global/LanguagePage.cpp index 485d7fd4..fcd174bd 100644 --- a/launcher/ui/pages/global/LanguagePage.cpp +++ b/launcher/ui/pages/global/LanguagePage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/global/LanguagePage.h b/launcher/ui/pages/global/LanguagePage.h index 9b321170..2fd4ab0d 100644 --- a/launcher/ui/pages/global/LanguagePage.h +++ b/launcher/ui/pages/global/LanguagePage.h @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 51303501..a6c98c08 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index b0cd405f..d929a0ea 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index 2cf17b32..51163e28 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index d6303cdd..c3bde612 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 731dd85f..ff30dd82 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index e43a087c..85e1f752 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "ModPage.h" #include "Application.h" #include "ui_ModPage.h" diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 0d8e1dcf..06e9db4f 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 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 "ListModel.h" #include "Application.h" diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 7cacf37a..07d1687c 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by From 40ccd1a46910012f80285f7b6982a5919e2a9dcf Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 4 Jun 2022 23:11:25 -0300 Subject: [PATCH 599/605] fix: handling of incomplete mods (i.e. mods without ModDetails that may have metadata) --- launcher/minecraft/mod/Mod.cpp | 44 ++++++++++++++++++++++++---------- launcher/minecraft/mod/Mod.h | 8 +++++-- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 71a32d32..39c7efd8 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -58,8 +58,6 @@ Mod::Mod(const QFileInfo& file) Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata) : m_file(mods_dir.absoluteFilePath(metadata.filename)) - // It is weird, but name is not reliable for comparing with the JAR files name - // FIXME: Maybe use hash when implemented? , m_internal_id(metadata.filename) , m_name(metadata.name) { @@ -131,7 +129,7 @@ auto Mod::enable(bool value) -> bool return false; } else { path += ".disabled"; - + if (!file.rename(path)) return false; } @@ -145,16 +143,22 @@ auto Mod::enable(bool value) -> bool void Mod::setStatus(ModStatus status) { - if(m_localDetails.get()) + if (m_localDetails) { m_localDetails->status = status; + } else { + m_temp_status = status; + } } void Mod::setMetadata(Metadata::ModStruct* metadata) { - if(status() == ModStatus::NoMetadata) + if (status() == ModStatus::NoMetadata) setStatus(ModStatus::Installed); - if(m_localDetails.get()) + if (m_localDetails) { m_localDetails->metadata.reset(metadata); + } else { + m_temp_metadata.reset(metadata); + } } auto Mod::destroy(QDir& index_dir) -> bool @@ -205,20 +209,36 @@ auto Mod::authors() const -> QStringList auto Mod::status() const -> ModStatus { + if (!m_localDetails) + return m_temp_status; return details().status; } +auto Mod::metadata() -> std::shared_ptr +{ + if (m_localDetails) + return m_localDetails->metadata; + return m_temp_metadata; +} + +auto Mod::metadata() const -> const std::shared_ptr +{ + if (m_localDetails) + return m_localDetails->metadata; + return m_temp_metadata; +} + void Mod::finishResolvingWithDetails(std::shared_ptr details) { m_resolving = false; m_resolved = true; m_localDetails = details; - if (status() != ModStatus::NoMetadata - && m_temp_metadata.get() - && m_temp_metadata->isValid() && - m_localDetails.get()) { - - m_localDetails->metadata.swap(m_temp_metadata); + if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) { + m_localDetails->metadata = m_temp_metadata; + if (status() == ModStatus::NoMetadata) + setStatus(ModStatus::Installed); } + + setStatus(m_temp_status); } diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 96d471b4..5f9c4684 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -73,8 +73,8 @@ public: auto authors() const -> QStringList; auto status() const -> ModStatus; - auto metadata() const -> const std::shared_ptr { return details().metadata; }; - auto metadata() -> std::shared_ptr { return m_localDetails->metadata; }; + auto metadata() -> std::shared_ptr; + auto metadata() const -> const std::shared_ptr; void setStatus(ModStatus status); void setMetadata(Metadata::ModStruct* metadata); @@ -109,6 +109,10 @@ protected: /* If the mod has metadata, this will be filled in the constructor, and passed to * the ModDetails when calling finishResolvingWithDetails */ std::shared_ptr m_temp_metadata; + + /* Set the mod status while it doesn't have local details just yet */ + ModStatus m_temp_status = ModStatus::NotInstalled; + std::shared_ptr m_localDetails; bool m_enabled = true; From 9f1f37e78023d66ce01481c05fa73db9eba0882a Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 7 Jun 2022 22:32:13 -0300 Subject: [PATCH 600/605] fix: correctly handle disabled mods with metadata im stupid --- .../minecraft/mod/tasks/ModFolderLoadTask.cpp | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 62d856f6..bde32b3e 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -53,12 +53,31 @@ void ModFolderLoadTask::run() m_mods_dir.refresh(); for (auto entry : m_mods_dir.entryInfoList()) { Mod mod(entry); - if(m_result->mods.contains(mod.internal_id())){ - m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); + + if (mod.enabled()) { + if (m_result->mods.contains(mod.internal_id())) { + m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); + } + else { + m_result->mods[mod.internal_id()] = mod; + m_result->mods[mod.internal_id()].setStatus(ModStatus::NoMetadata); + } } - else { - m_result->mods[mod.internal_id()] = mod; - m_result->mods[mod.internal_id()].setStatus(ModStatus::NoMetadata); + else { + QString chopped_id = mod.internal_id().chopped(9); + if (m_result->mods.contains(chopped_id)) { + m_result->mods[mod.internal_id()] = mod; + + auto metadata = m_result->mods[chopped_id].metadata(); + mod.setMetadata(new Metadata::ModStruct(*metadata)); + + m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); + m_result->mods.remove(chopped_id); + } + else { + m_result->mods[mod.internal_id()] = mod; + m_result->mods[mod.internal_id()].setStatus(ModStatus::NoMetadata); + } } } From 4448418b63715bc64acbb19bd75bedf725cb4165 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 12 Jun 2022 09:44:03 -0300 Subject: [PATCH 601/605] fix: segfault when the same mod is present enabled and disabled at once This maintains the previous behaviour --- launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index bde32b3e..80242fef 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -69,10 +69,12 @@ void ModFolderLoadTask::run() m_result->mods[mod.internal_id()] = mod; auto metadata = m_result->mods[chopped_id].metadata(); - mod.setMetadata(new Metadata::ModStruct(*metadata)); + if (metadata) { + mod.setMetadata(new Metadata::ModStruct(*metadata)); - m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); - m_result->mods.remove(chopped_id); + m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); + m_result->mods.remove(chopped_id); + } } else { m_result->mods[mod.internal_id()] = mod; From 2ce4ce90640edb23c22667eb4a6dc95f514e7fdd Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 12 Jun 2022 14:51:24 +0200 Subject: [PATCH 602/605] fix(installer): add version info to installer --- program_info/win_install.nsi.in | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index 596e3b57..cdd68d00 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -101,6 +101,13 @@ OutFile "../@Launcher_CommonName@-Setup.exe" ;-------------------------------- +; Version info +VIProductVersion "@Launcher_RELEASE_VERSION_NAME@" +VIFileVersion "@Launcher_RELEASE_VERSION_NAME@" +VIAddVersionKey "FileVersion" "@Launcher_RELEASE_VERSION_NAME@" + +;-------------------------------- + ; The stuff to install Section "@Launcher_CommonName@" From 278219b1d8bbd9b468f329fa8097fa243e155358 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 12 Jun 2022 16:24:05 +0200 Subject: [PATCH 603/605] fix(installer): use Windows version number format --- program_info/win_install.nsi.in | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index cdd68d00..e5687de7 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -102,9 +102,9 @@ OutFile "../@Launcher_CommonName@-Setup.exe" ;-------------------------------- ; Version info -VIProductVersion "@Launcher_RELEASE_VERSION_NAME@" -VIFileVersion "@Launcher_RELEASE_VERSION_NAME@" -VIAddVersionKey "FileVersion" "@Launcher_RELEASE_VERSION_NAME@" +VIProductVersion "@Launcher_RELEASE_VERSION_NAME4@" +VIFileVersion "@Launcher_RELEASE_VERSION_NAME4@" +VIAddVersionKey "FileVersion" "@Launcher_RELEASE_VERSION_NAME4@" ;-------------------------------- @@ -140,7 +140,7 @@ Section "@Launcher_CommonName@" WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S' WriteRegStr HKCU "${UNINST_KEY}" "InstallLocation" "$INSTDIR" WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "@Launcher_CommonName@ Contributors" - WriteRegStr HKCU "${UNINST_KEY}" "ProductVersion" "@Launcher_RELEASE_VERSION_NAME@" + WriteRegStr HKCU "${UNINST_KEY}" "ProductVersion" "@Launcher_RELEASE_VERSION_NAME4@" ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 IntFmt $0 "0x%08X" $0 WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0" From c04e38d01135a42c00b500cbb2b113d6824c37de Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sun, 12 Jun 2022 19:13:19 +0200 Subject: [PATCH 604/605] update macos runner to macos 12 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db7bd653..d8fc1ff2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: name: "Windows-x86_64" msystem: mingw64 - - os: macos-11 + - os: macos-12 macosx_deployment_target: 10.13 runs-on: ${{ matrix.os }} From 4be9e6a0bc0a4ac6b47ead7008b8a6a811c63b4d Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 13 Jun 2022 22:03:12 +0200 Subject: [PATCH 605/605] refactor: make is_indexed false by default Co-authored-by: flow --- launcher/minecraft/mod/ModFolderModel.h | 2 +- launcher/minecraft/mod/ResourcePackFolderModel.cpp | 2 +- launcher/minecraft/mod/TexturePackFolderModel.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index fcedae96..24b4d358 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -73,7 +73,7 @@ public: Enable, Toggle }; - ModFolderModel(const QString &dir, bool is_indexed); + ModFolderModel(const QString &dir, bool is_indexed = false); virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index fb1f1d29..276804ed 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -35,7 +35,7 @@ #include "ResourcePackFolderModel.h" -ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir, false) { +ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) { } QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 644dfd77..e3a22219 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -35,7 +35,7 @@ #include "TexturePackFolderModel.h" -TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir, false) { +TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) { } QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {