aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.yml9
-rw-r--r--.github/workflows/build.yml21
-rw-r--r--.github/workflows/trigger_builds.yml2
-rw-r--r--.github/workflows/winget.yml2
-rw-r--r--.markdownlint.yaml12
-rw-r--r--.markdownlintignore2
-rw-r--r--CMakeLists.txt25
-rw-r--r--CODE_OF_CONDUCT.md2
-rw-r--r--CONTRIBUTING.md55
-rw-r--r--COPYING.md113
-rw-r--r--README.md16
-rw-r--r--buildconfig/BuildConfig.cpp.in14
-rw-r--r--buildconfig/BuildConfig.h10
-rw-r--r--launcher/Application.cpp41
-rw-r--r--launcher/Application.h9
-rw-r--r--launcher/BaseInstance.cpp21
-rw-r--r--launcher/BaseInstance.h24
-rw-r--r--launcher/CMakeLists.txt23
-rw-r--r--launcher/FileSystem.cpp1
-rw-r--r--launcher/InstanceImportTask.cpp9
-rw-r--r--launcher/InstancePageProvider.h6
-rw-r--r--launcher/LaunchController.cpp20
-rw-r--r--launcher/LoggedProcess.cpp32
-rw-r--r--launcher/LoggedProcess.h5
-rw-r--r--launcher/MMCZip.cpp6
-rw-r--r--launcher/NullInstance.h6
-rw-r--r--launcher/QObjectPtr.h90
-rw-r--r--launcher/java/JavaUtils.cpp12
-rw-r--r--launcher/minecraft/Library.cpp3
-rw-r--r--launcher/minecraft/MinecraftInstance.cpp148
-rw-r--r--launcher/minecraft/MinecraftInstance.h30
-rw-r--r--launcher/minecraft/MinecraftUpdate.cpp10
-rw-r--r--launcher/minecraft/MinecraftUpdate.h2
-rw-r--r--launcher/minecraft/auth/MinecraftAccount.cpp2
-rw-r--r--launcher/minecraft/launch/DirectJavaLaunch.cpp4
-rw-r--r--launcher/minecraft/launch/LauncherPartLaunch.cpp2
-rw-r--r--launcher/minecraft/mod/Mod.cpp180
-rw-r--r--launcher/minecraft/mod/Mod.h70
-rw-r--r--launcher/minecraft/mod/ModDetails.h61
-rw-r--r--launcher/minecraft/mod/ModFolderModel.cpp624
-rw-r--r--launcher/minecraft/mod/ModFolderModel.h100
-rw-r--r--launcher/minecraft/mod/ModFolderModel_test.cpp92
-rw-r--r--launcher/minecraft/mod/Resource.cpp147
-rw-r--r--launcher/minecraft/mod/Resource.h115
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.cpp522
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.h326
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel_test.cpp275
-rw-r--r--launcher/minecraft/mod/ResourcePack.h13
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.cpp22
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.h9
-rw-r--r--launcher/minecraft/mod/ShaderPackFolderModel.h10
-rw-r--r--launcher/minecraft/mod/TexturePackFolderModel.cpp22
-rw-r--r--launcher/minecraft/mod/TexturePackFolderModel.h6
-rw-r--r--launcher/minecraft/mod/tasks/BasicFolderLoadTask.h53
-rw-r--r--launcher/minecraft/mod/tasks/LocalModParseTask.cpp140
-rw-r--r--launcher/minecraft/mod/tasks/LocalModParseTask.h22
-rw-r--r--launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp23
-rw-r--r--launcher/minecraft/mod/tasks/ModFolderLoadTask.h13
-rw-r--r--launcher/minecraft/mod/testdata/supercoolmod.jar1
-rw-r--r--launcher/minecraft/update/FMLLibrariesTask.cpp3
-rw-r--r--launcher/modplatform/EnsureMetadataTask.cpp124
-rw-r--r--launcher/modplatform/EnsureMetadataTask.h21
-rw-r--r--launcher/modplatform/ModIndex.cpp36
-rw-r--r--launcher/modplatform/ModIndex.h4
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.cpp144
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.h10
-rw-r--r--launcher/modplatform/atlauncher/ATLPackManifest.cpp64
-rw-r--r--launcher/modplatform/atlauncher/ATLPackManifest.h23
-rw-r--r--launcher/modplatform/helpers/HashUtils.cpp81
-rw-r--r--launcher/modplatform/helpers/HashUtils.h47
-rw-r--r--launcher/modplatform/modpacksch/FTBPackInstallTask.cpp9
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp39
-rw-r--r--launcher/net/Download.cpp4
-rw-r--r--launcher/net/Download.h2
-rw-r--r--launcher/net/HttpMetaCache.cpp23
-rw-r--r--launcher/net/HttpMetaCache.h17
-rw-r--r--launcher/net/MetaCacheSink.cpp45
-rw-r--r--launcher/net/MetaCacheSink.h3
-rw-r--r--launcher/net/NetAction.h2
-rw-r--r--launcher/net/NetJob.cpp202
-rw-r--r--launcher/net/NetJob.h48
-rw-r--r--launcher/net/Upload.cpp2
-rw-r--r--launcher/settings/SettingsObject.h1
-rw-r--r--launcher/tasks/ConcurrentTask.cpp38
-rw-r--r--launcher/tasks/ConcurrentTask.h4
-rw-r--r--launcher/tasks/MultipleOptionsTask.cpp41
-rw-r--r--launcher/tasks/MultipleOptionsTask.h14
-rw-r--r--launcher/tasks/SequentialTask.cpp102
-rw-r--r--launcher/tasks/SequentialTask.h60
-rw-r--r--launcher/tasks/Task.cpp24
-rw-r--r--launcher/tasks/Task.h12
-rw-r--r--launcher/tasks/Task_test.cpp125
-rw-r--r--launcher/ui/MainWindow.cpp3
-rw-r--r--launcher/ui/dialogs/AboutDialog.cpp11
-rw-r--r--launcher/ui/dialogs/AboutDialog.ui24
-rw-r--r--launcher/ui/dialogs/BlockedModsDialog.cpp28
-rw-r--r--launcher/ui/dialogs/BlockedModsDialog.h22
-rw-r--r--launcher/ui/dialogs/BlockedModsDialog.ui84
-rw-r--r--launcher/ui/dialogs/LoginDialog.cpp2
-rw-r--r--launcher/ui/dialogs/MSALoginDialog.cpp2
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.cpp2
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.cpp17
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.h4
-rw-r--r--launcher/ui/dialogs/NewInstanceDialog.cpp2
-rw-r--r--launcher/ui/dialogs/OfflineLoginDialog.cpp2
-rw-r--r--launcher/ui/pages/global/AccountListPage.cpp2
-rw-r--r--launcher/ui/pages/global/MinecraftPage.cpp10
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.cpp142
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.h13
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.ui6
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.cpp14
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp87
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.h15
-rw-r--r--launcher/ui/pages/instance/ResourcePackPage.h6
-rw-r--r--launcher/ui/pages/instance/ShaderPackPage.h6
-rw-r--r--launcher/ui/pages/instance/TexturePackPage.h6
-rw-r--r--launcher/ui/pages/instance/VersionPage.cpp6
-rw-r--r--launcher/ui/pages/instance/VersionPage.ui6
-rw-r--r--launcher/ui/pages/instance/WorldListPage.cpp12
-rw-r--r--launcher/ui/pages/instance/WorldListPage.h2
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp5
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp59
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlPage.h6
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp95
-rw-r--r--launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h56
-rw-r--r--launcher/ui/pages/modplatform/legacy_ftb/Page.ui18
-rw-r--r--launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp5
-rw-r--r--launcher/ui/widgets/InfoFrame.cpp (renamed from launcher/ui/widgets/MCModInfoFrame.cpp)98
-rw-r--r--launcher/ui/widgets/InfoFrame.h (renamed from launcher/ui/widgets/MCModInfoFrame.h)36
-rw-r--r--launcher/ui/widgets/InfoFrame.ui (renamed from launcher/ui/widgets/MCModInfoFrame.ui)8
-rw-r--r--launcher/updater/UpdateChecker.cpp3
-rw-r--r--launcher/updater/UpdateChecker.h2
-rw-r--r--libraries/README.md26
-rw-r--r--libraries/katabasis/README.md6
-rw-r--r--libraries/katabasis/acknowledgements.md38
-rw-r--r--libraries/murmur2/src/MurmurHash2.cpp172
-rw-r--r--libraries/murmur2/src/MurmurHash2.h39
-rw-r--r--libraries/tomlc99/README.md25
-rw-r--r--nix/NIX.md27
-rw-r--r--program_info/CMakeLists.txt2
-rw-r--r--program_info/README.md1
-rw-r--r--program_info/org.polymc.PolyMC.metainfo.xml.in22
-rw-r--r--program_info/polymc.manifest.in2
-rw-r--r--program_info/polymc.rc.in6
-rw-r--r--program_info/win_install.nsi.in12
145 files changed, 3981 insertions, 2333 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index bac73932..ab3c8a29 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -27,7 +27,14 @@ body:
attributes:
label: Version of PolyMC
description: The version of PolyMC used in the bug report.
- placeholder: PolyMC 1.3.2
+ placeholder: PolyMC 1.4.1
+ validations:
+ required: true
+- type: textarea
+ attributes:
+ label: Version of Qt
+ description: The version of Qt used in the bug report. You can find it in Help -> About PolyMC -> About Qt.
+ placeholder: Qt 6.3.0
validations:
required: true
- type: textarea
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0599c1d9..26820d47 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -27,7 +27,6 @@ jobs:
qt_host: linux
qt_version: '6.2.4'
qt_modules: 'qt5compat qtimageformats'
- qt_path: /home/runner/work/PolyMC/Qt
- os: windows-2022
name: "Windows-Legacy"
@@ -43,9 +42,8 @@ jobs:
macosx_deployment_target: 10.14
qt_ver: 6
qt_host: mac
- qt_version: '6.3.1'
+ qt_version: '6.3.0'
qt_modules: 'qt5compat qtimageformats'
- qt_path: /Users/runner/work/PolyMC/Qt
runs-on: ${{ matrix.os }}
@@ -141,24 +139,16 @@ jobs:
run: |
sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5
- - name: Cache Qt (macOS and AppImage)
- id: cache-qt
- if: matrix.qt_ver == 6 && runner.os != 'Windows'
- uses: actions/cache@v3
- with:
- path: '${{ matrix.qt_path }}/${{ matrix.qt_version }}'
- key: ${{ matrix.qt_host }}-${{ matrix.qt_version }}-"${{ matrix.qt_modules }}"-qt_cache
-
- name: Install Qt (macOS and AppImage)
if: matrix.qt_ver == 6 && runner.os != 'Windows'
- uses: jurplel/install-qt-action@v2
+ uses: jurplel/install-qt-action@v3
with:
version: ${{ matrix.qt_version }}
host: ${{ matrix.qt_host }}
target: 'desktop'
modules: ${{ matrix.qt_modules }}
- cached: ${{ steps.cache-qt.outputs.cache-hit }}
- aqtversion: ==2.1.*
+ cache: true
+ cache-key-prefix: ${{ matrix.qt_host }}-${{ matrix.qt_version }}-"${{ matrix.qt_modules }}"-qt_cache
- name: Prepare AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5
@@ -314,6 +304,9 @@ jobs:
cp -r ${{ github.workspace }}/JREs/jre17/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk
cp -r /home/runner/work/PolyMC/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
+
+ cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
+ cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}//usr/lib/
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"
diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml
index ee9eb4ea..55b4fdd4 100644
--- a/.github/workflows/trigger_builds.yml
+++ b/.github/workflows/trigger_builds.yml
@@ -11,6 +11,7 @@ on:
- '**.nix'
- 'packages/**'
- '.github/ISSUE_TEMPLATE/**'
+ - '.markdownlint**'
pull_request:
paths-ignore:
- '**.md'
@@ -19,6 +20,7 @@ on:
- '**.nix'
- 'packages/**'
- '.github/ISSUE_TEMPLATE/**'
+ - '.markdownlint**'
workflow_dispatch:
jobs:
diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml
index b8ecce13..98981e80 100644
--- a/.github/workflows/winget.yml
+++ b/.github/workflows/winget.yml
@@ -10,5 +10,5 @@ jobs:
- uses: vedantmgoyal2009/winget-releaser@latest
with:
identifier: PolyMC.PolyMC
- installers-regex: '\.exe$'
+ installers-regex: 'PolyMC-Windows-Setup-.+\.exe$'
token: ${{ secrets.WINGET_TOKEN }}
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
new file mode 100644
index 00000000..5781edb0
--- /dev/null
+++ b/.markdownlint.yaml
@@ -0,0 +1,12 @@
+# MD013/line-length - Line length
+MD013: false
+
+# MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content
+MD024:
+ siblings-only: true
+
+# MD033/no-inline-html Inline HTML
+MD033: false
+
+# MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading
+MD041: false
diff --git a/.markdownlintignore b/.markdownlintignore
new file mode 100644
index 00000000..a8669d01
--- /dev/null
+++ b/.markdownlintignore
@@ -0,0 +1,2 @@
+libraries/nbtplusplus
+libraries/quazip
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 62724323..6cb806a3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -79,12 +79,12 @@ set(Launcher_NEWS_OPEN_URL "https://polymc.org/news" CACHE STRING "URL that gets
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)
-set(Launcher_VERSION_MINOR 4)
-set(Launcher_VERSION_HOTFIX 0)
+set(Launcher_VERSION_MAJOR 5)
+set(Launcher_VERSION_MINOR 0)
-# Build number
-set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.")
+set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
+set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.0.0")
+set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},0,0")
# Build platform.
set(Launcher_BUILD_PLATFORM "" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.")
@@ -143,15 +143,8 @@ message(STATUS "Git commit: ${Launcher_GIT_COMMIT}")
message(STATUS "Git tag: ${Launcher_GIT_TAG}")
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}")
-
-#### Custom target to just print the version.
-add_custom_target(version echo "Version: ${Launcher_RELEASE_VERSION_NAME}")
-add_custom_target(tcversion echo "\\#\\#teamcity[setParameter name=\\'env.LAUNCHER_VERSION\\' value=\\'${Launcher_RELEASE_VERSION_NAME}\\']")
+set(Launcher_BUILD_TIMESTAMP "${TODAY}")
################################ 3rd Party Libs ################################
@@ -226,9 +219,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}")
- 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_BUNDLE_VERSION "${Launcher_VERSION_NAME}")
+ set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_NAME}")
+ set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_NAME}")
set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns)
set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2021-2022 ${Launcher_Copyright}")
set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "idALcUIazingvKSSsEa9U7coDVxZVx/ORpOEE/QtJfg=")
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 52a9f30a..7bbd01da 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,4 +1,5 @@
# Contributor Covenant Code of Conduct
+
This is a modified version of the Contributor Covenant.
See commit history to see our changes.
@@ -133,4 +134,3 @@ For answers to common questions about this code of conduct, see the FAQ at
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
-
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 216549c6..4bca126f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -6,6 +6,7 @@ Try to follow the existing formatting.
If there is no existing formatting, you may use `clang-format` with our included `.clang-format` configuration.
In general, in order of importance:
+
- Make sure your IDE is not messing up line endings or whitespace and avoid using linters.
- Prefer readability over dogma.
- Keep to the existing formatting.
@@ -26,37 +27,37 @@ Signed-off-by: Author name <Author email>
By signing off your work, you agree to the terms below:
- Developer's Certificate of Origin 1.1
-
- By making a contribution to this project, I certify that:
-
- (a) The contribution was created in whole or in part by me and I
- have the right to submit it under the open source license
- indicated in the file; or
-
- (b) The contribution is based upon previous work that, to the best
- of my knowledge, is covered under an appropriate open source
- license and I have the right under that license to submit that
- work with modifications, whether created in whole or in part
- by me, under the same open source license (unless I am
- permitted to submit under a different license), as indicated
- in the file; or
-
- (c) The contribution was provided directly to me by some other
- person who certified (a), (b) or (c) and I have not modified
- it.
-
- (d) I understand and agree that this project and the contribution
- are public and that a record of the contribution (including all
- personal information I submit with it, including my sign-off) is
- maintained indefinitely and may be redistributed consistent with
- this project or the open source license(s) involved.
+```
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+ have the right to submit it under the open source license
+ indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+ of my knowledge, is covered under an appropriate open source
+ license and I have the right under that license to submit that
+ work with modifications, whether created in whole or in part
+ by me, under the same open source license (unless I am
+ permitted to submit under a different license), as indicated
+ in the file; or
+
+(c) The contribution was provided directly to me by some other
+ person who certified (a), (b) or (c) and I have not modified
+ it.
+
+(d) I understand and agree that this project and the contribution
+ are public and that a record of the contribution (including all
+ personal information I submit with it, including my sign-off) is
+ maintained indefinitely and may be redistributed consistent with
+ this project or the open source license(s) involved.
+```
These terms will be enforced once you create a pull request, and you will be informed automatically if any of your commits aren't signed-off by you.
As a bonus, you can also [cryptographically sign your commits][gh-signing-commits] and enable [vigilant mode][gh-vigilant-mode] on GitHub.
-
-
[gh-signing-commits]: https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits
[gh-vigilant-mode]: https://docs.github.com/en/authentication/managing-commit-signature-verification/displaying-verification-statuses-for-all-of-your-commits
diff --git a/COPYING.md b/COPYING.md
index 1dd18e17..c94c51c3 100644
--- a/COPYING.md
+++ b/COPYING.md
@@ -1,4 +1,4 @@
-# PolyMC
+## PolyMC
PolyMC - Minecraft Launcher
Copyright (C) 2021-2022 PolyMC Contributors
@@ -32,36 +32,56 @@
See the License for the specific language governing permissions and
limitations under the License.
-# MinGW runtime (Windows)
+## MinGW-w64 runtime (Windows)
- Copyright (c) 2012 MinGW.org project
+ Copyright (c) 2009, 2010, 2011, 2012, 2013 by the mingw-w64 project
- Permission is hereby granted, free of charge, to any person obtaining a
- copy of this software and associated documentation files (the "Software"),
- to deal in the Software without restriction, including without limitation
- the rights to use, copy, modify, merge, publish, distribute, sublicense,
- and/or sell copies of the Software, and to permit persons to whom the
- Software is furnished to do so, subject to the following conditions:
+ This license has been certified as open source. It has also been designated
+ as GPL compatible by the Free Software Foundation (FSF).
- The above copyright notice, this permission notice and the below disclaimer
- shall be included in all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- DEALINGS IN THE SOFTWARE.
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
-# Qt 5/6
+ 1. Redistributions in source code must retain the accompanying copyright
+ notice, this list of conditions, and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the accompanying
+ copyright notice, this list of conditions, and the following disclaimer
+ in the documentation and/or other materials provided with the
+ distribution.
+ 3. Names of the copyright holders must not be used to endorse or promote
+ products derived from this software without prior written permission
+ from the copyright holders.
+ 4. The right to distribute this software or to use it for any purpose does
+ not give you the right to use Servicemarks (sm) or Trademarks (tm) of
+ the copyright holders. Use of them is covered by separate agreement
+ with the copyright holders.
+ 5. If any files are modified, you must cause the modified files to carry
+ prominent notices stating that you changed the files and the date of
+ any change.
+
+ Disclaimer
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+ OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ Information on third party licenses used in MinGW-w64 can be found in its COPYING.MinGW-w64-runtime.txt.
+
+## Qt 5/6
Copyright (C) 2022 The Qt Company Ltd and other contributors.
Contact: https://www.qt.io/licensing
Licensed under LGPL v3
-# libnbt++
+## libnbt++
libnbt++ - A library for the Minecraft Named Binary Tag format.
Copyright (C) 2013, 2015 ljfa-ag
@@ -79,7 +99,7 @@
You should have received a copy of the GNU Lesser General Public License
along with libnbt++. If not, see <http://www.gnu.org/licenses/>.
-# rainbow (KGuiAddons)
+## rainbow (KGuiAddons)
Copyright (C) 2007 Matthew Woehlke <mw_triad@users.sourceforge.net>
Copyright (C) 2007 Olaf Schmidt <ojschmidt@kde.org>
@@ -102,7 +122,7 @@
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
-# Hoedown
+## Hoedown
Copyright (c) 2008, Natacha Porté
Copyright (c) 2011, Vicent Martí
@@ -120,7 +140,7 @@
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-# Batch icon set
+## Batch icon set
You are free to use Batch (the "icon set") or any part thereof (the "icons")
in any personal, open-source or commercial work without obligation of payment
@@ -136,7 +156,7 @@
PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THE USE OF THE ICONS,
EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
-# Material Design Icons
+## Material Design Icons
Copyright (c) 2014, Austin Andrews (http://materialdesignicons.com/),
with Reserved Font Name Material Design Icons.
@@ -147,7 +167,7 @@
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-# Quazip
+## Quazip
Copyright (C) 2005-2021 Sergey A. Tachenov
@@ -171,7 +191,7 @@
See COPYING file for the full LGPL text.
-# xz-minidec
+## xz-minidec
XZ decompressor
@@ -181,7 +201,7 @@
This file has been put into the public domain.
You can do whatever you want with this file.
-# ColumnResizer
+## ColumnResizer
Copyright (c) 2011-2016 Aurélien Gâteau and contributors.
@@ -217,7 +237,7 @@
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-# launcher (`libraries/launcher`)
+## launcher (`libraries/launcher`)
PolyMC - Minecraft Launcher
Copyright (C) 2021-2022 PolyMC Contributors
@@ -268,7 +288,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-# lionshead
+## lionshead
Code has been taken from https://github.com/natefoo/lionshead and loosely
translated to C++ laced with Qt.
@@ -295,7 +315,7 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-# tomlc99
+## tomlc99
MIT License
@@ -320,7 +340,7 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-# O2 (Katabasis fork)
+## O2 (Katabasis fork)
Copyright (c) 2012, Akos Polster
All rights reserved.
@@ -345,3 +365,32 @@
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+## Gamemode
+
+ Copyright (c) 2017-2022, Feral Interactive
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of Feral Interactive nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
index 69639e5b..6ff868e0 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
- <p align="center">
+<p align="center">
<img src="./program_info/polymc-header-black.svg#gh-light-mode-only" alt="PolyMC logo" width="50%"/>
<img src="./program_info/polymc-header.svg#gh-dark-mode-only" alt="PolyMC logo" width="50%"/>
</p>
@@ -12,8 +12,7 @@ If you want to read about why this fork was created, check out [our FAQ page](ht
# Installation
- All downloads and instructions for PolyMC can be found [here](https://polymc.org/download/)
-- Last build status: https://github.com/PolyMC/PolyMC/actions
-
+- Last build status: <https://github.com/PolyMC/PolyMC/actions>
## Development Builds
@@ -60,7 +59,7 @@ If you want to build PolyMC yourself, check [Build Instructions](https://polymc.
## Translations
-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
+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 information
@@ -69,14 +68,16 @@ 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/custom build as long as you follow the terms of the [license](LICENSE) (this is a legal responsibility), and if you made code changes rather than just packaging a custom build, please do the following as a basic courtesy:
-- Make it clear that your fork is not PolyMC and is not endorsed by or affiliated with the PolyMC project (https://polymc.org).
+
+- Make it clear that your fork is not PolyMC and is not endorsed by or affiliated with the PolyMC project (<https://polymc.org>).
- 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.
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)
+
+- [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 (`""`).
@@ -85,6 +86,7 @@ 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)
diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in
index 7da66f36..50e5e8a4 100644
--- a/buildconfig/BuildConfig.cpp.in
+++ b/buildconfig/BuildConfig.cpp.in
@@ -55,10 +55,9 @@ Config::Config()
// Version information
VERSION_MAJOR = @Launcher_VERSION_MAJOR@;
VERSION_MINOR = @Launcher_VERSION_MINOR@;
- VERSION_HOTFIX = @Launcher_VERSION_HOTFIX@;
- VERSION_BUILD = @Launcher_VERSION_BUILD@;
BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@";
+ BUILD_DATE = "@Launcher_BUILD_TIMESTAMP@";
UPDATER_BASE = "@Launcher_UPDATER_BASE@";
MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@";
@@ -85,7 +84,7 @@ Config::Config()
{
VERSION_CHANNEL = GIT_REFSPEC;
VERSION_CHANNEL.remove("refs/heads/");
- if(!UPDATER_BASE.isEmpty() && !BUILD_PLATFORM.isEmpty() && VERSION_BUILD >= 0) {
+ if(!UPDATER_BASE.isEmpty() && !BUILD_PLATFORM.isEmpty()) {
UPDATER_ENABLED = true;
}
}
@@ -98,7 +97,6 @@ Config::Config()
VERSION_CHANNEL = "unknown";
}
- VERSION_STR = "@Launcher_VERSION_STRING@";
NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@";
NEWS_OPEN_URL = "@Launcher_NEWS_OPEN_URL@";
HELP_URL = "@Launcher_HELP_URL@";
@@ -116,7 +114,7 @@ Config::Config()
QString Config::versionString() const
{
- return QString("%1.%2.%3").arg(VERSION_MAJOR).arg(VERSION_MINOR).arg(VERSION_HOTFIX);
+ return QString("%1.%2").arg(VERSION_MAJOR).arg(VERSION_MINOR);
}
QString Config::printableVersionString() const
@@ -128,11 +126,5 @@ QString Config::printableVersionString() const
{
vstr += "-" + VERSION_CHANNEL;
}
-
- // if a build number is set, also add it to the end
- if(VERSION_BUILD >= 0)
- {
- vstr += "+build." + QString::number(VERSION_BUILD);
- }
return vstr;
}
diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h
index 95786d82..de66cec4 100644
--- a/buildconfig/BuildConfig.h
+++ b/buildconfig/BuildConfig.h
@@ -55,10 +55,6 @@ class Config {
int VERSION_MAJOR;
/// The minor version number.
int VERSION_MINOR;
- /// The hotfix number.
- int VERSION_HOTFIX;
- /// The build number.
- int VERSION_BUILD;
/**
* The version channel
@@ -71,6 +67,9 @@ class Config {
/// A short string identifying this build's platform. For example, "lin64" or "win32".
QString BUILD_PLATFORM;
+ /// A string containing the build timestamp
+ QString BUILD_DATE;
+
/// URL for the updater's channel
QString UPDATER_BASE;
@@ -95,9 +94,6 @@ class Config {
/// The git refspec of this build
QString GIT_REFSPEC;
- /// This is printed on start to standard output
- QString VERSION_STR;
-
/**
* This is used to fetch the news RSS feed.
* It defaults in CMakeLists.txt to "https://multimc.org/rss.xml"
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index 5066fd0b..aa937964 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -113,6 +113,11 @@
#include <sys.h>
+#ifdef Q_OS_LINUX
+#include <dlfcn.h>
+#include "gamemode_client.h"
+#endif
+
#if defined Q_OS_WIN32
#ifndef WIN32_LEAN_AND_MEAN
@@ -321,7 +326,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
{
// Root path is used for updates and portable data
-#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
+#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr
m_rootPath = foo.absolutePath();
#elif defined(Q_OS_WIN32)
@@ -776,7 +781,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
auto platform = getIdealPlatform(BuildConfig.BUILD_PLATFORM);
auto channelUrl = BuildConfig.UPDATER_BASE + platform + "/channels.json";
qDebug() << "Initializing updater with platform: " << platform << " -- " << channelUrl;
- m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_CHANNEL, BuildConfig.VERSION_BUILD));
+ m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_CHANNEL));
qDebug() << "<> Updater started.";
}
@@ -866,6 +871,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("FlameMods", QDir("cache/FlameMods").absolutePath());
m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath());
m_metacache->addBase("root", QDir::currentPath());
m_metacache->addBase("translations", QDir("translations").absolutePath());
@@ -921,6 +927,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
{
return;
}
+
+ updateCapabilities();
performMainStartupAction();
}
@@ -1260,6 +1268,9 @@ bool Application::launch(
}
connect(controller.get(), &LaunchController::succeeded, this, &Application::controllerSucceeded);
connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed);
+ connect(controller.get(), &LaunchController::aborted, this, [this] {
+ controllerFailed(tr("Aborted"));
+ });
addRunningInstance();
controller->start();
return true;
@@ -1566,14 +1577,30 @@ shared_qobject_ptr<Meta::Index> Application::metadataIndex()
return m_metadataIndex;
}
-Application::Capabilities Application::currentCapabilities()
+void Application::updateCapabilities()
{
- Capabilities c;
+ m_capabilities = None;
if (!getMSAClientID().isEmpty())
- c |= SupportsMSA;
+ m_capabilities |= SupportsMSA;
if (!getFlameAPIKey().isEmpty())
- c |= SupportsFlame;
- return c;
+ m_capabilities |= SupportsFlame;
+
+#ifdef Q_OS_LINUX
+ if (gamemode_query_status() >= 0)
+ m_capabilities |= SupportsGameMode;
+
+ {
+ void *dummy = dlopen("libMangoHud_dlsym.so", RTLD_LAZY);
+ // try normal variant as well
+ if (dummy == NULL)
+ dummy = dlopen("libMangoHud.so", RTLD_LAZY);
+
+ if (dummy != NULL) {
+ dlclose(dummy);
+ m_capabilities |= SupportsMangoHud;
+ }
+ }
+#endif
}
QString Application::getJarPath(QString jarFile)
diff --git a/launcher/Application.h b/launcher/Application.h
index 019c3c3d..41fd4c47 100644
--- a/launcher/Application.h
+++ b/launcher/Application.h
@@ -95,6 +95,8 @@ public:
SupportsMSA = 1 << 0,
SupportsFlame = 1 << 1,
+ SupportsGameMode = 1 << 2,
+ SupportsMangoHud = 1 << 3,
};
Q_DECLARE_FLAGS(Capabilities, Capability)
@@ -162,7 +164,7 @@ public:
shared_qobject_ptr<Meta::Index> metadataIndex();
- Capabilities currentCapabilities();
+ void updateCapabilities();
/*!
* Finds and returns the full path to a jar file.
@@ -180,6 +182,10 @@ public:
return m_rootPath;
}
+ const Capabilities capabilities() {
+ return m_capabilities;
+ }
+
/*!
* Opens a json file using either a system default editor, or, if not empty, the editor
* specified in the settings
@@ -258,6 +264,7 @@ private:
QString m_rootPath;
Status m_status = Application::StartingUp;
+ Capabilities m_capabilities;
#ifdef Q_OS_MACOS
Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive;
diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp
index 5a84a931..e6d4d8e3 100644
--- a/launcher/BaseInstance.cpp
+++ b/launcher/BaseInstance.cpp
@@ -53,15 +53,22 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s
: QObject()
{
m_settings = settings;
+ m_global_settings = globalSettings;
m_rootDir = rootDir;
m_settings->registerSetting("name", "Unnamed Instance");
m_settings->registerSetting("iconKey", "default");
m_settings->registerSetting("notes", "");
+
m_settings->registerSetting("lastLaunchTime", 0);
m_settings->registerSetting("totalTimePlayed", 0);
m_settings->registerSetting("lastTimePlayed", 0);
+ // Game time override
+ auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false);
+ m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride);
+ m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"), gameTimeOverride);
+
// NOTE: Sometimees InstanceType is already registered, as it was used to identify the type of
// a locally stored instance
if (!m_settings->getSetting("InstanceType"))
@@ -149,7 +156,7 @@ void BaseInstance::setManagedPack(const QString& type, const QString& id, const
int BaseInstance::getConsoleMaxLines() const
{
- auto lineSetting = settings()->getSetting("ConsoleMaxLines");
+ auto lineSetting = m_settings->getSetting("ConsoleMaxLines");
bool conversionOk = false;
int maxLines = lineSetting->get().toInt(&conversionOk);
if(!conversionOk)
@@ -162,7 +169,7 @@ int BaseInstance::getConsoleMaxLines() const
bool BaseInstance::shouldStopOnConsoleOverflow() const
{
- return settings()->get("ConsoleOverflowStop").toBool();
+ return m_settings->get("ConsoleOverflowStop").toBool();
}
void BaseInstance::iconUpdated(QString key)
@@ -237,7 +244,7 @@ void BaseInstance::setRunning(bool running)
int64_t BaseInstance::totalTimePlayed() const
{
- qint64 current = settings()->get("totalTimePlayed").toLongLong();
+ qint64 current = m_settings->get("totalTimePlayed").toLongLong();
if(m_isRunning)
{
QDateTime timeNow = QDateTime::currentDateTime();
@@ -253,7 +260,7 @@ int64_t BaseInstance::lastTimePlayed() const
QDateTime timeNow = QDateTime::currentDateTime();
return m_timeStarted.secsTo(timeNow);
}
- return settings()->get("lastTimePlayed").toLongLong();
+ return m_settings->get("lastTimePlayed").toLongLong();
}
void BaseInstance::resetTimePlayed()
@@ -272,8 +279,10 @@ QString BaseInstance::instanceRoot() const
return m_rootDir;
}
-SettingsObjectPtr BaseInstance::settings() const
+SettingsObjectPtr BaseInstance::settings()
{
+ loadSpecificSettings();
+
return m_settings;
}
@@ -340,7 +349,7 @@ QString BaseInstance::windowTitle() const
}
// FIXME: why is this here? move it to MinecraftInstance!!!
-QStringList BaseInstance::extraArguments() const
+QStringList BaseInstance::extraArguments()
{
return Commandline::splitArgs(settings()->get("JvmArgs").toString());
}
diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h
index 2a94dcc6..3af104e9 100644
--- a/launcher/BaseInstance.h
+++ b/launcher/BaseInstance.h
@@ -154,7 +154,7 @@ public:
return level;
};
- virtual QStringList extraArguments() const;
+ virtual QStringList extraArguments();
/// Traits. Normally inside the version, depends on instance implementation.
virtual QSet <QString> traits() const = 0;
@@ -170,9 +170,18 @@ public:
/*!
* \brief Gets this instance's settings object.
* This settings object stores instance-specific settings.
+ *
+ * Note that this method is not const.
+ * It may call loadSpecificSettings() to ensure those are loaded.
+ *
* \return A pointer to this instance's settings object.
*/
- virtual SettingsObjectPtr settings() const;
+ virtual SettingsObjectPtr settings();
+
+ /*!
+ * \brief Loads settings specific to an instance type if they're not already loaded.
+ */
+ virtual void loadSpecificSettings() = 0;
/// returns a valid update task
virtual Task::Ptr createUpdateTask(Net::Mode mode) = 0;
@@ -206,7 +215,7 @@ public:
virtual QString instanceConfigFolder() const = 0;
/// get variables this instance exports
- virtual QMap<QString, QString> getVariables() const = 0;
+ virtual QMap<QString, QString> getVariables() = 0;
virtual QString typeName() const = 0;
@@ -268,6 +277,11 @@ public:
protected:
void changeStatus(Status newStatus);
+ SettingsObjectPtr globalSettings() const { return m_global_settings.lock(); };
+
+ bool isSpecificSettingsLoaded() const { return m_specific_settings_loaded; }
+ void setSpecificSettingsLoaded(bool loaded) { m_specific_settings_loaded = loaded; }
+
signals:
/*!
* \brief Signal emitted when properties relevant to the instance view change
@@ -296,6 +310,10 @@ private: /* data */
bool m_crashed = false;
bool m_hasUpdate = false;
bool m_hasBrokenVersion = false;
+
+ SettingsObjectWeakPtr m_global_settings;
+ bool m_specific_settings_loaded = false;
+
};
Q_DECLARE_METATYPE(shared_qobject_ptr<BaseInstance>)
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 492a4b9d..dfd56d26 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -318,10 +318,16 @@ set(MINECRAFT_SOURCES
minecraft/mod/ModDetails.h
minecraft/mod/ModFolderModel.h
minecraft/mod/ModFolderModel.cpp
+ minecraft/mod/Resource.h
+ minecraft/mod/Resource.cpp
+ minecraft/mod/ResourceFolderModel.h
+ minecraft/mod/ResourceFolderModel.cpp
minecraft/mod/ResourcePackFolderModel.h
minecraft/mod/ResourcePackFolderModel.cpp
minecraft/mod/TexturePackFolderModel.h
minecraft/mod/TexturePackFolderModel.cpp
+ minecraft/mod/ShaderPackFolderModel.h
+ minecraft/mod/tasks/BasicFolderLoadTask.h
minecraft/mod/tasks/ModFolderLoadTask.h
minecraft/mod/tasks/ModFolderLoadTask.cpp
minecraft/mod/tasks/LocalModParseTask.h
@@ -375,8 +381,8 @@ ecm_add_test(minecraft/Library_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VER
# FIXME: shares data with FileSystem test
# TODO: needs testdata
-ecm_add_test(minecraft/mod/ModFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
- TEST_NAME ModFolderModel)
+ecm_add_test(minecraft/mod/ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
+ TEST_NAME ResourceFolderModel)
ecm_add_test(minecraft/ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME ParseUtils)
@@ -494,6 +500,8 @@ set(API_SOURCES
modplatform/modrinth/ModrinthAPI.cpp
modplatform/helpers/NetworkModAPI.h
modplatform/helpers/NetworkModAPI.cpp
+ modplatform/helpers/HashUtils.h
+ modplatform/helpers/HashUtils.cpp
)
set(FTB_SOURCES
@@ -764,6 +772,8 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h
ui/pages/modplatform/atlauncher/AtlPage.cpp
ui/pages/modplatform/atlauncher/AtlPage.h
+ ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
+ ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h
ui/pages/modplatform/ftb/FtbFilterModel.cpp
ui/pages/modplatform/ftb/FtbFilterModel.h
@@ -849,6 +859,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ModDownloadDialog.h
ui/dialogs/ScrollMessageBox.cpp
ui/dialogs/ScrollMessageBox.h
+ ui/dialogs/BlockedModsDialog.cpp
+ ui/dialogs/BlockedModsDialog.h
ui/dialogs/ChooseProviderDialog.h
ui/dialogs/ChooseProviderDialog.cpp
ui/dialogs/ModUpdateDialog.cpp
@@ -875,8 +887,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/LineSeparator.h
ui/widgets/LogView.cpp
ui/widgets/LogView.h
- ui/widgets/MCModInfoFrame.cpp
- ui/widgets/MCModInfoFrame.h
+ ui/widgets/InfoFrame.cpp
+ ui/widgets/InfoFrame.h
ui/widgets/ModFilterWidget.cpp
ui/widgets/ModFilterWidget.h
ui/widgets/ModListView.cpp
@@ -940,7 +952,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/pages/modplatform/technic/TechnicPage.ui
ui/widgets/InstanceCardWidget.ui
ui/widgets/CustomCommands.ui
- ui/widgets/MCModInfoFrame.ui
+ ui/widgets/InfoFrame.ui
ui/widgets/ModFilterWidget.ui
ui/dialogs/CopyInstanceDialog.ui
ui/dialogs/ProfileSetupDialog.ui
@@ -960,6 +972,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/EditAccountDialog.ui
ui/dialogs/ReviewMessageBox.ui
ui/dialogs/ScrollMessageBox.ui
+ ui/dialogs/BlockedModsDialog.ui
ui/dialogs/ChooseProviderDialog.ui
)
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
index 21edbb48..8eeb2885 100644
--- a/launcher/FileSystem.cpp
+++ b/launcher/FileSystem.cpp
@@ -467,6 +467,7 @@ bool overrideFolder(QString overwritten_path, QString override_path)
for (auto file : listFolderPaths(root_override)) {
QString destination = file;
destination.replace(override_path, overwritten_path);
+ ensureFilePathExists(destination);
qDebug() << QString("Applying override %1 in %2").arg(file, destination);
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
index 14e1cd47..de0afc96 100644
--- a/launcher/InstanceImportTask.cpp
+++ b/launcher/InstanceImportTask.cpp
@@ -60,7 +60,7 @@
#include "net/ChecksumValidator.h"
#include "ui/dialogs/CustomMessageBox.h"
-#include "ui/dialogs/ScrollMessageBox.h"
+#include "ui/dialogs/BlockedModsDialog.h"
#include <algorithm>
@@ -396,21 +396,24 @@ void InstanceImportTask::processFlame()
auto results = m_modIdResolver->getResults();
//first check for blocked mods
QString text;
+ QList<QUrl> urls;
auto anyBlocked = false;
for(const auto& result: results.files.values()) {
if (!result.resolved || result.url.isEmpty()) {
text += QString("%1: <a href='%2'>%2</a><br/>").arg(result.fileName, result.websiteUrl);
+ urls.append(QUrl(result.websiteUrl));
anyBlocked = true;
}
}
if(anyBlocked) {
qWarning() << "Blocked mods found, displaying mod list";
- auto message_dialog = new ScrollMessageBox(m_parent,
+ auto message_dialog = new BlockedModsDialog(m_parent,
tr("Blocked mods found"),
tr("The following mods were blocked on third party launchers.<br/>"
"You will need to manually download them and add them to the modpack"),
- text);
+ text,
+ urls);
message_dialog->setModal(true);
if (message_dialog->exec()) {
diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h
index 78fb7016..bf29377d 100644
--- a/launcher/InstancePageProvider.h
+++ b/launcher/InstancePageProvider.h
@@ -37,9 +37,9 @@ public:
modsPage->setFilter("%1 (*.zip *.jar *.litemod)");
values.append(modsPage);
values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList()));
- values.append(new ResourcePackPage(onesix.get()));
- values.append(new TexturePackPage(onesix.get()));
- values.append(new ShaderPackPage(onesix.get()));
+ values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList()));
+ values.append(new TexturePackPage(onesix.get(), onesix->texturePackList()));
+ values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList()));
values.append(new NotesPage(onesix.get()));
values.append(new WorldListPage(onesix.get(), onesix->worldList()));
values.append(new ServersPage(onesix));
diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp
index d36ee3fe..11f9b2bb 100644
--- a/launcher/LaunchController.cpp
+++ b/launcher/LaunchController.cpp
@@ -145,16 +145,26 @@ void LaunchController::login() {
return;
}
- // we try empty password first :)
- QString password;
// we loop until the user succeeds in logging in or gives up
bool tryagain = true;
- // the failure. the default failure.
- const QString needLoginAgain = tr("Your account is currently not logged in. Please enter your password to log in again. <br /> <br /> This could be caused by a password change.");
- QString failReason = needLoginAgain;
+ unsigned int tries = 0;
while (tryagain)
{
+ if (tries > 0 && tries % 3 == 0) {
+ auto result = QMessageBox::question(
+ m_parentWidget,
+ tr("Continue launch?"),
+ tr("It looks like we couldn't launch after %1 tries. Do you want to continue trying?")
+ .arg(tries)
+ );
+
+ if (result == QMessageBox::No) {
+ emitAborted();
+ return;
+ }
+ }
+ tries++;
m_session = std::make_shared<AuthSession>();
m_session->wants_online = m_online;
m_accountToUse->fillSession(m_session);
diff --git a/launcher/LoggedProcess.cpp b/launcher/LoggedProcess.cpp
index fbdeed8f..6447f5c6 100644
--- a/launcher/LoggedProcess.cpp
+++ b/launcher/LoggedProcess.cpp
@@ -34,8 +34,9 @@
*/
#include "LoggedProcess.h"
-#include "MessageLevel.h"
#include <QDebug>
+#include <QTextDecoder>
+#include "MessageLevel.h"
LoggedProcess::LoggedProcess(QObject *parent) : QProcess(parent)
{
@@ -59,25 +60,26 @@ LoggedProcess::~LoggedProcess()
}
}
-QStringList reprocess(const QByteArray & data, QString & leftover)
+QStringList reprocess(const QByteArray& data, QTextDecoder& decoder)
{
- QString str = leftover + QString::fromLocal8Bit(data);
-
- str.remove('\r');
- QStringList lines = str.split("\n");
- leftover = lines.takeLast();
+ auto str = decoder.toUnicode(data);
+#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
+ auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed, QString::SkipEmptyParts);
+#else
+ auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed, Qt::SkipEmptyParts);
+#endif
return lines;
}
void LoggedProcess::on_stdErr()
{
- auto lines = reprocess(readAllStandardError(), m_err_leftover);
+ auto lines = reprocess(readAllStandardError(), m_err_decoder);
emit log(lines, MessageLevel::StdErr);
}
void LoggedProcess::on_stdOut()
{
- auto lines = reprocess(readAllStandardOutput(), m_out_leftover);
+ auto lines = reprocess(readAllStandardOutput(), m_out_decoder);
emit log(lines, MessageLevel::StdOut);
}
@@ -86,18 +88,6 @@ void LoggedProcess::on_exit(int exit_code, QProcess::ExitStatus status)
// save the exit code
m_exit_code = exit_code;
- // Flush console window
- if (!m_err_leftover.isEmpty())
- {
- emit log({m_err_leftover}, MessageLevel::StdErr);
- m_err_leftover.clear();
- }
- if (!m_out_leftover.isEmpty())
- {
- emit log({m_err_leftover}, MessageLevel::StdOut);
- m_out_leftover.clear();
- }
-
// based on state, send signals
if (!m_is_aborting)
{
diff --git a/launcher/LoggedProcess.h b/launcher/LoggedProcess.h
index 61e74bd9..2360d1ea 100644
--- a/launcher/LoggedProcess.h
+++ b/launcher/LoggedProcess.h
@@ -36,6 +36,7 @@
#pragma once
#include <QProcess>
+#include <QTextDecoder>
#include "MessageLevel.h"
/*
@@ -88,8 +89,8 @@ private:
void changeState(LoggedProcess::State state);
private:
- QString m_err_leftover;
- QString m_out_leftover;
+ QTextDecoder m_err_decoder = QTextDecoder(QTextCodec::codecForLocale());
+ QTextDecoder m_out_decoder = QTextDecoder(QTextCodec::codecForLocale());
bool m_killed = false;
State m_state = NotRunning;
int m_exit_code = 0;
diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp
index 04ca5094..9f4e968f 100644
--- a/launcher/MMCZip.cpp
+++ b/launcher/MMCZip.cpp
@@ -148,7 +148,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
// do not merge disabled mods.
if (!mod->enabled())
continue;
- if (mod->type() == Mod::MOD_ZIPFILE)
+ if (mod->type() == ResourceType::ZIPFILE)
{
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles))
{
@@ -158,7 +158,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
return false;
}
}
- else if (mod->type() == Mod::MOD_SINGLEFILE)
+ else if (mod->type() == ResourceType::SINGLEFILE)
{
// FIXME: buggy - does not work with addedFiles
auto filename = mod->fileinfo();
@@ -171,7 +171,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
}
addedFiles.insert(filename.fileName());
}
- else if (mod->type() == Mod::MOD_FOLDER)
+ else if (mod->type() == ResourceType::FOLDER)
{
// untested, but seems to be unused / not possible to reach
// FIXME: buggy - does not work with addedFiles
diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h
index 9b0a9331..53e64a05 100644
--- a/launcher/NullInstance.h
+++ b/launcher/NullInstance.h
@@ -15,6 +15,10 @@ public:
void saveNow() override
{
}
+ void loadSpecificSettings() override
+ {
+ setSpecificSettingsLoaded(true);
+ }
QString getStatusbarDescription() override
{
return tr("Unknown instance type");
@@ -43,7 +47,7 @@ public:
{
return QProcessEnvironment();
}
- QMap<QString, QString> getVariables() const override
+ QMap<QString, QString> getVariables() override
{
return QMap<QString, QString>();
}
diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h
index 173dc5e7..b1ef1c8d 100644
--- a/launcher/QObjectPtr.h
+++ b/launcher/QObjectPtr.h
@@ -1,91 +1,37 @@
#pragma once
+#include <QObject>
+#include <QSharedPointer>
+
#include <functional>
#include <memory>
-#include <QObject>
-namespace details
-{
-struct DeleteQObjectLater
-{
- void operator()(QObject *obj) const
- {
- obj->deleteLater();
- }
-};
-}
/**
* A unique pointer class with unique pointer semantics intended for derivates of QObject
* Calls deleteLater() instead of destroying the contained object immediately
*/
-template<typename T> using unique_qobject_ptr = std::unique_ptr<T, details::DeleteQObjectLater>;
+template <typename T>
+using unique_qobject_ptr = QScopedPointer<T, QScopedPointerDeleteLater>;
/**
* A shared pointer class with shared pointer semantics intended for derivates of QObject
* Calls deleteLater() instead of destroying the contained object immediately
*/
template <typename T>
-class shared_qobject_ptr
-{
-public:
- shared_qobject_ptr(){}
- shared_qobject_ptr(T * wrap)
- {
- reset(wrap);
- }
- shared_qobject_ptr(const shared_qobject_ptr<T>& other)
- {
- m_ptr = other.m_ptr;
- }
- template<typename Derived>
- shared_qobject_ptr(const shared_qobject_ptr<Derived> &other)
- {
- m_ptr = other.unwrap();
- }
+class shared_qobject_ptr : public QSharedPointer<T> {
+ public:
+ constexpr shared_qobject_ptr() : QSharedPointer<T>() {}
+ constexpr shared_qobject_ptr(T* ptr) : QSharedPointer<T>(ptr, &QObject::deleteLater) {}
+ constexpr shared_qobject_ptr(std::nullptr_t null_ptr) : QSharedPointer<T>(null_ptr, &QObject::deleteLater) {}
-public:
- void reset(T * wrap)
- {
- using namespace std::placeholders;
- m_ptr.reset(wrap, std::bind(&QObject::deleteLater, _1));
- }
- void reset(const shared_qobject_ptr<T> &other)
- {
- m_ptr = other.m_ptr;
- }
- void reset()
- {
- m_ptr.reset();
- }
- T * get() const
- {
- return m_ptr.get();
- }
- T * operator->() const
- {
- return m_ptr.get();
- }
- T & operator*() const
- {
- return *m_ptr.get();
- }
- operator bool() const
- {
- return m_ptr.get() != nullptr;
- }
- const std::shared_ptr <T> unwrap() const
+ template <typename Derived>
+ constexpr shared_qobject_ptr(const shared_qobject_ptr<Derived>& other) : QSharedPointer<T>(other)
+ {}
+
+ void reset() { QSharedPointer<T>::reset(); }
+ void reset(const shared_qobject_ptr<T>& other)
{
- return m_ptr;
+ shared_qobject_ptr<T> t(other);
+ this->swap(t);
}
- template<typename U>
- bool operator==(const shared_qobject_ptr<U>& other) const {
- return m_ptr == other.m_ptr;
- }
- template<typename U>
- bool operator!=(const shared_qobject_ptr<U>& other) const {
- return m_ptr != other.m_ptr;
- }
-
-private:
- std::shared_ptr <T> m_ptr;
};
diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp
index 2b19fca0..2f91605b 100644
--- a/launcher/java/JavaUtils.cpp
+++ b/launcher/java/JavaUtils.cpp
@@ -174,11 +174,17 @@ JavaInstallPtr JavaUtils::GetDefaultJava()
QStringList addJavasFromEnv(QList<QString> javas)
{
- QByteArray env = qgetenv("POLYMC_JAVA_PATHS");
+ auto env = qEnvironmentVariable("POLYMC_JAVA_PATHS");
#if defined(Q_OS_WIN32)
- QList<QString> javaPaths = QString::fromLocal8Bit(env).replace("\\", "/").split(QLatin1String(";"));
+ QList<QString> javaPaths = env.replace("\\", "/").split(QLatin1String(";"));
+
+ auto envPath = qEnvironmentVariable("PATH");
+ QList<QString> javaPathsfromPath = envPath.replace("\\", "/").split(QLatin1String(";"));
+ for (QString string : javaPathsfromPath) {
+ javaPaths.append(string + "/javaw.exe");
+ }
#else
- QList<QString> javaPaths = QString::fromLocal8Bit(env).split(QLatin1String(":"));
+ QList<QString> javaPaths = env.split(QLatin1String(":"));
#endif
for(QString i : javaPaths)
{
diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp
index c7982705..ba7aed4b 100644
--- a/launcher/minecraft/Library.cpp
+++ b/launcher/minecraft/Library.cpp
@@ -88,6 +88,9 @@ QList<NetAction::Ptr> Library::getDownloads(
options |= Net::Download::Option::AcceptLocalFiles;
}
+ // Don't add a time limit for the libraries cache entry validity
+ options |= Net::Download::Option::MakeEternal;
+
if(sha1.size())
{
auto rawSha1 = QByteArray::fromHex(sha1.toLatin1());
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index 5a6f8de0..9478b1b8 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -76,6 +76,7 @@
#include "mod/ModFolderModel.h"
#include "mod/ResourcePackFolderModel.h"
+#include "mod/ShaderPackFolderModel.h"
#include "mod/TexturePackFolderModel.h"
#include "WorldList.h"
@@ -115,6 +116,19 @@ private:
MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir)
: BaseInstance(globalSettings, settings, rootDir)
{
+ m_components.reset(new PackProfile(this));
+}
+
+void MinecraftInstance::saveNow()
+{
+ m_components->saveNow();
+}
+
+void MinecraftInstance::loadSpecificSettings()
+{
+ if (isSpecificSettingsLoaded())
+ return;
+
// Java Settings
auto javaOverride = m_settings->registerSetting("OverrideJava", false);
auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false);
@@ -124,64 +138,58 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO
auto javaOrLocation = std::make_shared<OrSetting>("JavaOrLocationOverride", javaOverride, locationOverride);
auto javaOrArgs = std::make_shared<OrSetting>("JavaOrArgsOverride", javaOverride, argsOverride);
- 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);
- m_settings->registerPassthrough(globalSettings->getSetting("JavaVersion"), javaOrLocation);
- m_settings->registerPassthrough(globalSettings->getSetting("JavaArchitecture"), javaOrLocation);
-
- // Window Size
- auto windowSetting = m_settings->registerSetting("OverrideWindow", false);
- m_settings->registerOverride(globalSettings->getSetting("LaunchMaximized"), windowSetting);
- m_settings->registerOverride(globalSettings->getSetting("MinecraftWinWidth"), windowSetting);
- m_settings->registerOverride(globalSettings->getSetting("MinecraftWinHeight"), windowSetting);
-
- // Memory
- auto memorySetting = m_settings->registerSetting("OverrideMemory", false);
- m_settings->registerOverride(globalSettings->getSetting("MinMemAlloc"), memorySetting);
- m_settings->registerOverride(globalSettings->getSetting("MaxMemAlloc"), memorySetting);
- m_settings->registerOverride(globalSettings->getSetting("PermGen"), memorySetting);
-
- // Minecraft launch method
- auto launchMethodOverride = m_settings->registerSetting("OverrideMCLaunchMethod", false);
- m_settings->registerOverride(globalSettings->getSetting("MCLaunchMethod"), launchMethodOverride);
-
- // Native library workarounds
- auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false);
- m_settings->registerOverride(globalSettings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride);
- m_settings->registerOverride(globalSettings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride);
-
- // Peformance related options
- auto performanceOverride = m_settings->registerSetting("OverridePerformance", false);
- m_settings->registerOverride(globalSettings->getSetting("EnableFeralGamemode"), performanceOverride);
- m_settings->registerOverride(globalSettings->getSetting("EnableMangoHud"), performanceOverride);
- m_settings->registerOverride(globalSettings->getSetting("UseDiscreteGpu"), performanceOverride);
-
- // Game time
- auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false);
- m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride);
- m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"), gameTimeOverride);
+ if (auto global_settings = globalSettings()) {
+ m_settings->registerOverride(global_settings->getSetting("JavaPath"), javaOrLocation);
+ m_settings->registerOverride(global_settings->getSetting("JvmArgs"), javaOrArgs);
+ m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), javaOrLocation);
+
+ // special!
+ m_settings->registerPassthrough(global_settings->getSetting("JavaTimestamp"), javaOrLocation);
+ m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation);
+ m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), javaOrLocation);
+
+ // Window Size
+ auto windowSetting = m_settings->registerSetting("OverrideWindow", false);
+ m_settings->registerOverride(global_settings->getSetting("LaunchMaximized"), windowSetting);
+ m_settings->registerOverride(global_settings->getSetting("MinecraftWinWidth"), windowSetting);
+ m_settings->registerOverride(global_settings->getSetting("MinecraftWinHeight"), windowSetting);
+
+ // Memory
+ auto memorySetting = m_settings->registerSetting("OverrideMemory", false);
+ m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting);
+ m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting);
+ m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting);
+
+ // Minecraft launch method
+ auto launchMethodOverride = m_settings->registerSetting("OverrideMCLaunchMethod", false);
+ m_settings->registerOverride(global_settings->getSetting("MCLaunchMethod"), launchMethodOverride);
+
+ // Native library workarounds
+ auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false);
+ m_settings->registerOverride(global_settings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride);
+ m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride);
+
+ // Peformance related options
+ auto performanceOverride = m_settings->registerSetting("OverridePerformance", false);
+ m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride);
+ m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride);
+ m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride);
+
+ // Miscellaneous
+ auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false);
+ m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride);
+ m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride);
+
+ m_settings->set("InstanceType", "OneSix");
+ }
// Join server on launch, this does not have a global override
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_settings->set("InstanceType", "OneSix");
+ qDebug() << "Instance-type specific settings were loaded!";
- m_components.reset(new PackProfile(this));
-}
-
-void MinecraftInstance::saveNow()
-{
- m_components->saveNow();
+ setSpecificSettingsLoaded(true);
}
QString MinecraftInstance::typeName() const
@@ -308,7 +316,7 @@ QDir MinecraftInstance::versionsPath() const
return QDir::current().absoluteFilePath("versions");
}
-QStringList MinecraftInstance::getClassPath() const
+QStringList MinecraftInstance::getClassPath()
{
QStringList jars, nativeJars;
auto javaArchitecture = settings()->get("JavaArchitecture").toString();
@@ -323,7 +331,7 @@ QString MinecraftInstance::getMainClass() const
return profile->getMainClass();
}
-QStringList MinecraftInstance::getNativeJars() const
+QStringList MinecraftInstance::getNativeJars()
{
QStringList jars, nativeJars;
auto javaArchitecture = settings()->get("JavaArchitecture").toString();
@@ -332,7 +340,7 @@ QStringList MinecraftInstance::getNativeJars() const
return nativeJars;
}
-QStringList MinecraftInstance::extraArguments() const
+QStringList MinecraftInstance::extraArguments()
{
auto list = BaseInstance::extraArguments();
auto version = getPackProfile();
@@ -358,7 +366,7 @@ QStringList MinecraftInstance::extraArguments() const
return list;
}
-QStringList MinecraftInstance::javaArguments() const
+QStringList MinecraftInstance::javaArguments()
{
QStringList args;
@@ -415,7 +423,7 @@ QStringList MinecraftInstance::javaArguments() const
return args;
}
-QMap<QString, QString> MinecraftInstance::getVariables() const
+QMap<QString, QString> MinecraftInstance::getVariables()
{
QMap<QString, QString> out;
out.insert("INST_NAME", name());
@@ -447,13 +455,11 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
QProcessEnvironment env = createEnvironment();
#ifdef Q_OS_LINUX
- if (settings()->get("EnableMangoHud").toBool())
+ if (settings()->get("EnableMangoHud").toBool() && APPLICATION->capabilities() & Application::SupportsMangoHud)
{
auto preload = env.value("LD_PRELOAD", "") + ":libMangoHud_dlsym.so:libMangoHud.so";
- auto lib_path = env.value("LD_LIBRARY_PATH", "") + ":/usr/local/$LIB/mangohud/:/usr/$LIB/mangohud/";
env.insert("LD_PRELOAD", preload);
- env.insert("LD_LIBRARY_PATH", lib_path);
env.insert("MANGOHUD", "1");
}
@@ -707,7 +713,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
});
for(auto mod: modList)
{
- if(mod->type() == Mod::MOD_FOLDER)
+ if(mod->type() == ResourceType::FOLDER)
{
out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)";
continue;
@@ -943,9 +949,9 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(new CreateGameFolders(pptr));
}
- if (!serverToJoin && m_settings->get("JoinServerOnLaunch").toBool())
+ if (!serverToJoin && settings()->get("JoinServerOnLaunch").toBool())
{
- QString fullAddress = m_settings->get("JoinServerOnLaunchAddress").toString();
+ QString fullAddress = settings()->get("JoinServerOnLaunchAddress").toString();
serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(fullAddress)));
}
@@ -1053,10 +1059,10 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
QString MinecraftInstance::launchMethod()
{
- return m_settings->get("MCLaunchMethod").toString();
+ return settings()->get("MCLaunchMethod").toString();
}
-JavaVersion MinecraftInstance::getJavaVersion() const
+JavaVersion MinecraftInstance::getJavaVersion()
{
return JavaVersion(settings()->get("JavaVersion").toString());
}
@@ -1085,18 +1091,18 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const
return m_core_mod_list;
}
-std::shared_ptr<ModFolderModel> MinecraftInstance::resourcePackList() const
+std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList() const
{
if (!m_resource_pack_list)
{
m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir()));
- m_resource_pack_list->disableInteraction(isRunning());
- connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ModFolderModel::disableInteraction);
+ m_resource_pack_list->enableInteraction(!isRunning());
+ connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ResourcePackFolderModel::disableInteraction);
}
return m_resource_pack_list;
}
-std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
+std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList() const
{
if (!m_texture_pack_list)
{
@@ -1107,11 +1113,11 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
return m_texture_pack_list;
}
-std::shared_ptr<ModFolderModel> MinecraftInstance::shaderPackList() const
+std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList() const
{
if (!m_shader_pack_list)
{
- m_shader_pack_list.reset(new ResourcePackFolderModel(shaderPacksDir()));
+ m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir()));
m_shader_pack_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_shader_pack_list.get(), &ModFolderModel::disableInteraction);
}
diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h
index 8e1c67f2..d62ac655 100644
--- a/launcher/minecraft/MinecraftInstance.h
+++ b/launcher/minecraft/MinecraftInstance.h
@@ -7,6 +7,10 @@
#include "minecraft/launch/MinecraftServerTarget.h"
class ModFolderModel;
+class ResourceFolderModel;
+class ResourcePackFolderModel;
+class ShaderPackFolderModel;
+class TexturePackFolderModel;
class WorldList;
class GameOptions;
class LaunchStep;
@@ -20,6 +24,8 @@ public:
virtual ~MinecraftInstance() {};
virtual void saveNow() override;
+ void loadSpecificSettings() override;
+
// FIXME: remove
QString typeName() const override;
// FIXME: remove
@@ -70,24 +76,24 @@ public:
////// Mod Lists //////
std::shared_ptr<ModFolderModel> loaderModList() const;
std::shared_ptr<ModFolderModel> coreModList() const;
- std::shared_ptr<ModFolderModel> resourcePackList() const;
- std::shared_ptr<ModFolderModel> texturePackList() const;
- std::shared_ptr<ModFolderModel> shaderPackList() const;
+ std::shared_ptr<ResourcePackFolderModel> resourcePackList() const;
+ std::shared_ptr<TexturePackFolderModel> texturePackList() const;
+ std::shared_ptr<ShaderPackFolderModel> shaderPackList() const;
std::shared_ptr<WorldList> worldList() const;
std::shared_ptr<GameOptions> gameOptionsModel() const;
////// Launch stuff //////
Task::Ptr createUpdateTask(Net::Mode mode) override;
shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override;
- QStringList extraArguments() const override;
+ QStringList extraArguments() override;
QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override;
QList<Mod*> getJarMods() const;
QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin);
/// get arguments passed to java
- QStringList javaArguments() const;
+ QStringList javaArguments();
/// get variables for launch command variable substitution/environment
- QMap<QString, QString> getVariables() const override;
+ QMap<QString, QString> getVariables() override;
/// create an environment for launching processes
QProcessEnvironment createEnvironment() override;
@@ -103,16 +109,16 @@ public:
QString getStatusbarDescription() override;
// FIXME: remove
- virtual QStringList getClassPath() const;
+ virtual QStringList getClassPath();
// FIXME: remove
- virtual QStringList getNativeJars() const;
+ virtual QStringList getNativeJars();
// FIXME: remove
virtual QString getMainClass() const;
// FIXME: remove
virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) const;
- virtual JavaVersion getJavaVersion() const;
+ virtual JavaVersion getJavaVersion();
protected:
QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session);
@@ -123,9 +129,9 @@ protected: // data
std::shared_ptr<PackProfile> m_components;
mutable std::shared_ptr<ModFolderModel> m_loader_mod_list;
mutable std::shared_ptr<ModFolderModel> m_core_mod_list;
- mutable std::shared_ptr<ModFolderModel> m_resource_pack_list;
- mutable std::shared_ptr<ModFolderModel> m_shader_pack_list;
- mutable std::shared_ptr<ModFolderModel> m_texture_pack_list;
+ mutable std::shared_ptr<ResourcePackFolderModel> m_resource_pack_list;
+ mutable std::shared_ptr<ShaderPackFolderModel> m_shader_pack_list;
+ mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list;
mutable std::shared_ptr<WorldList> m_world_list;
mutable std::shared_ptr<GameOptions> m_game_options;
};
diff --git a/launcher/minecraft/MinecraftUpdate.cpp b/launcher/minecraft/MinecraftUpdate.cpp
index 0ce0c347..3a3aa864 100644
--- a/launcher/minecraft/MinecraftUpdate.cpp
+++ b/launcher/minecraft/MinecraftUpdate.cpp
@@ -43,7 +43,7 @@ void MinecraftUpdate::executeTask()
m_tasks.clear();
// create folders
{
- m_tasks.append(std::make_shared<FoldersTask>(m_inst));
+ m_tasks.append(new FoldersTask(m_inst));
}
// add metadata update task if necessary
@@ -53,23 +53,23 @@ void MinecraftUpdate::executeTask()
auto task = components->getCurrentTask();
if(task)
{
- m_tasks.append(task.unwrap());
+ m_tasks.append(task);
}
}
// libraries download
{
- m_tasks.append(std::make_shared<LibrariesTask>(m_inst));
+ m_tasks.append(new LibrariesTask(m_inst));
}
// FML libraries download and copy into the instance
{
- m_tasks.append(std::make_shared<FMLLibrariesTask>(m_inst));
+ m_tasks.append(new FMLLibrariesTask(m_inst));
}
// assets update
{
- m_tasks.append(std::make_shared<AssetUpdateTask>(m_inst));
+ m_tasks.append(new AssetUpdateTask(m_inst));
}
if(!m_preFailure.isEmpty())
diff --git a/launcher/minecraft/MinecraftUpdate.h b/launcher/minecraft/MinecraftUpdate.h
index acf2eb86..c9cf8624 100644
--- a/launcher/minecraft/MinecraftUpdate.h
+++ b/launcher/minecraft/MinecraftUpdate.h
@@ -50,7 +50,7 @@ private:
private:
MinecraftInstance *m_inst = nullptr;
- QList<std::shared_ptr<Task>> m_tasks;
+ QList<Task::Ptr> m_tasks;
QString m_preFailure;
int m_currentTask = -1;
bool m_abort = false;
diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp
index a5c6f542..73d570f1 100644
--- a/launcher/minecraft/auth/MinecraftAccount.cpp
+++ b/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -238,7 +238,7 @@ void MinecraftAccount::authFailed(QString reason)
}
bool MinecraftAccount::isActive() const {
- return m_currentTask;
+ return !m_currentTask.isNull();
}
bool MinecraftAccount::shouldRefresh() const {
diff --git a/launcher/minecraft/launch/DirectJavaLaunch.cpp b/launcher/minecraft/launch/DirectJavaLaunch.cpp
index 152485b3..ca55cd2e 100644
--- a/launcher/minecraft/launch/DirectJavaLaunch.cpp
+++ b/launcher/minecraft/launch/DirectJavaLaunch.cpp
@@ -21,6 +21,8 @@
#include <FileSystem.h>
#include <Commandline.h>
+#include "Application.h"
+
#ifdef Q_OS_LINUX
#include "gamemode_client.h"
#endif
@@ -86,7 +88,7 @@ void DirectJavaLaunch::executeTask()
}
#ifdef Q_OS_LINUX
- if (instance->settings()->get("EnableFeralGamemode").toBool())
+ if (instance->settings()->get("EnableFeralGamemode").toBool() && APPLICATION->capabilities() & Application::SupportsGameMode)
{
auto pid = m_process.processId();
if (pid)
diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp
index 63e4d90f..ce477ad7 100644
--- a/launcher/minecraft/launch/LauncherPartLaunch.cpp
+++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp
@@ -181,7 +181,7 @@ void LauncherPartLaunch::executeTask()
}
#ifdef Q_OS_LINUX
- if (instance->settings()->get("EnableFeralGamemode").toBool())
+ if (instance->settings()->get("EnableFeralGamemode").toBool() && APPLICATION->capabilities() & Application::SupportsGameMode)
{
auto pid = m_process.processId();
if (pid)
diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp
index 588d76e3..39023f69 100644
--- a/launcher/minecraft/mod/Mod.cpp
+++ b/launcher/minecraft/mod/Mod.cpp
@@ -36,130 +36,77 @@
#include "Mod.h"
+#include <QDebug>
#include <QDir>
#include <QString>
+#include <QRegularExpression>
-#include <FileSystem.h>
-#include <QDebug>
-
-#include "Application.h"
#include "MetadataHandler.h"
+#include "Version.h"
-namespace {
-
-ModDetails invalidDetails;
-
-}
-
-Mod::Mod(const QFileInfo& file)
+Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
{
- repath(file);
- m_changedDateTime = file.lastModified();
+ m_enabled = (file.suffix() != "disabled");
}
Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata)
- : m_file(mods_dir.absoluteFilePath(metadata.filename))
- , m_internal_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_enabled = true;
- m_changedDateTime = m_file.lastModified();
-
- m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
+ : Mod(mods_dir.absoluteFilePath(metadata.filename))
+{
+ m_name = metadata.name;
+ m_local_details.metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
}
-void Mod::repath(const QFileInfo& file)
+void Mod::setStatus(ModStatus status)
{
- m_file = file;
- QString name_base = file.fileName();
-
- m_type = Mod::MOD_UNKNOWN;
-
- m_internal_id = name_base;
-
- if (m_file.isDir()) {
- m_type = MOD_FOLDER;
- m_name = name_base;
- } else if (m_file.isFile()) {
- if (name_base.endsWith(".disabled")) {
- m_enabled = false;
- name_base.chop(9);
- } else {
- m_enabled = true;
- }
- if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) {
- m_type = MOD_ZIPFILE;
- name_base.chop(4);
- } else if (name_base.endsWith(".litemod")) {
- m_type = MOD_LITEMOD;
- name_base.chop(8);
- } else {
- m_type = MOD_SINGLEFILE;
- }
- m_name = name_base;
- }
+ m_local_details.status = status;
}
-
-auto Mod::enable(bool value) -> bool
+void Mod::setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata)
{
- if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER)
- return false;
-
- if (m_enabled == value)
- return false;
-
- QString path = m_file.absoluteFilePath();
- QFile file(path);
- if (value) {
- if (!path.endsWith(".disabled"))
- return false;
- path.chop(9);
-
- if (!file.rename(path))
- return false;
- } else {
- path += ".disabled";
-
- if (!file.rename(path))
- return false;
- }
-
if (status() == ModStatus::NoMetadata)
- repath(QFileInfo(path));
+ setStatus(ModStatus::Installed);
- m_enabled = value;
- return true;
+ m_local_details.metadata = metadata;
}
-void Mod::setStatus(ModStatus status)
+std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
{
- if (m_localDetails) {
- m_localDetails->status = status;
- } else {
- m_temp_status = status;
+ auto cast_other = dynamic_cast<Mod const*>(&other);
+ if (!cast_other)
+ return Resource::compare(other, type);
+
+ switch (type) {
+ default:
+ case SortType::ENABLED:
+ case SortType::NAME:
+ case SortType::DATE: {
+ auto res = Resource::compare(other, type);
+ if (res.first != 0)
+ return res;
+ }
+ case SortType::VERSION: {
+ auto this_ver = Version(version());
+ auto other_ver = Version(cast_other->version());
+ if (this_ver > other_ver)
+ return { 1, type == SortType::VERSION };
+ if (this_ver < other_ver)
+ return { -1, type == SortType::VERSION };
+ }
}
+ return { 0, false };
}
-void Mod::setMetadata(const Metadata::ModStruct& metadata)
+
+bool Mod::applyFilter(QRegularExpression filter) const
{
- if (status() == ModStatus::NoMetadata)
- setStatus(ModStatus::Installed);
+ if (filter.match(description()).hasMatch())
+ return true;
- if (m_localDetails) {
- m_localDetails->metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
- } else {
- m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
+ for (auto& author : authors()) {
+ if (filter.match(author).hasMatch()) {
+ return true;
+ }
}
+
+ return Resource::applyFilter(filter);
}
auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
@@ -175,13 +122,12 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
}
}
- m_type = MOD_UNKNOWN;
- return FS::deletePath(m_file.filePath());
+ return Resource::destroy();
}
auto Mod::details() const -> const ModDetails&
{
- return m_localDetails ? *m_localDetails : invalidDetails;
+ return m_local_details;
}
auto Mod::name() const -> QString
@@ -218,35 +164,29 @@ 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<Metadata::ModStruct>
{
- if (m_localDetails)
- return m_localDetails->metadata;
- return m_temp_metadata;
+ return m_local_details.metadata;
}
auto Mod::metadata() const -> const std::shared_ptr<Metadata::ModStruct>
{
- if (m_localDetails)
- return m_localDetails->metadata;
- return m_temp_metadata;
+ return m_local_details.metadata;
}
-void Mod::finishResolvingWithDetails(std::shared_ptr<ModDetails> details)
+void Mod::finishResolvingWithDetails(ModDetails&& details)
{
- m_resolving = false;
- m_resolved = true;
- m_localDetails = details;
+ m_is_resolving = false;
+ m_is_resolved = true;
- setStatus(m_temp_status);
+ std::shared_ptr<Metadata::ModStruct> metadata = details.metadata;
+ if (details.status == ModStatus::Unknown)
+ details.status = m_local_details.status;
- if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) {
- setMetadata(*m_temp_metadata);
- m_temp_metadata.reset();
- }
+ m_local_details = std::move(details);
+ if (metadata)
+ setMetadata(std::move(metadata));
}
diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h
index 7a13e44b..f336bec4 100644
--- a/launcher/minecraft/mod/Mod.h
+++ b/launcher/minecraft/mod/Mod.h
@@ -39,38 +39,23 @@
#include <QFileInfo>
#include <QList>
-#include "QObjectPtr.h"
+#include "Resource.h"
#include "ModDetails.h"
-class Mod : public QObject
+class Mod : public Resource
{
Q_OBJECT
public:
- enum ModType
- {
- MOD_UNKNOWN, //!< Indicates an unspecified mod type.
- 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
- };
-
using Ptr = shared_qobject_ptr<Mod>;
+ using WeakPtr = QPointer<Mod>;
Mod() = default;
Mod(const QFileInfo &file);
- explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
-
- 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 enabled() const -> bool { return m_enabled; }
-
- auto valid() const -> bool { return m_type != MOD_UNKNOWN; }
+ Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
+ Mod(QString file_path) : Mod(QFileInfo(file_path)) {}
auto details() const -> const ModDetails&;
- auto name() const -> QString;
+ auto name() const -> QString override;
auto version() const -> QString;
auto homeurl() const -> QString;
auto description() const -> QString;
@@ -81,46 +66,17 @@ public:
auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>;
void setStatus(ModStatus status);
- void setMetadata(const Metadata::ModStruct& metadata);
+ void setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata);
+ void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared<Metadata::ModStruct>(metadata)); }
- auto enable(bool value) -> bool;
+ [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
+ [[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
- // delete all the files of this mod
+ // Delete all the files of this mod
auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool;
- // change the mod's filesystem path (used by mod lists for *MAGIC* purposes)
- void repath(const QFileInfo &file);
-
- 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;
- m_resolutionTicket = resolutionTicket;
- }
- void finishResolvingWithDetails(std::shared_ptr<ModDetails> details);
+ void finishResolvingWithDetails(ModDetails&& details);
protected:
- QFileInfo m_file;
- QDateTime m_changedDateTime;
-
- QString m_internal_id;
- /* Name as reported via the file name */
- QString m_name;
- ModType m_type = MOD_UNKNOWN;
-
- /* If the mod has metadata, this will be filled in the constructor, and passed to
- * the ModDetails when calling finishResolvingWithDetails */
- std::shared_ptr<Metadata::ModStruct> m_temp_metadata;
-
- /* Set the mod status while it doesn't have local details just yet */
- ModStatus m_temp_status = ModStatus::NoMetadata;
-
- std::shared_ptr<ModDetails> m_localDetails;
-
- bool m_enabled = true;
- bool m_resolving = false;
- bool m_resolved = false;
- int m_resolutionTicket = 0;
+ ModDetails m_local_details;
};
diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h
index 3e0a7ab0..dd84b0a3 100644
--- a/launcher/minecraft/mod/ModDetails.h
+++ b/launcher/minecraft/mod/ModDetails.h
@@ -46,34 +46,77 @@ enum class ModStatus {
Installed, // Both JAR and Metadata are present
NotInstalled, // Only the Metadata is present
NoMetadata, // Only the JAR is present
+ Unknown, // Default status
};
struct ModDetails
{
/* Mod ID as defined in the ModLoader-specific metadata */
- QString mod_id;
+ QString mod_id = {};
/* Human-readable name */
- QString name;
+ QString name = {};
/* Human-readable mod version */
- QString version;
+ QString version = {};
/* Human-readable minecraft version */
- QString mcversion;
+ QString mcversion = {};
/* URL for mod's home page */
- QString homeurl;
+ QString homeurl = {};
/* Human-readable description */
- QString description;
+ QString description = {};
/* List of the author's names */
- QStringList authors;
+ QStringList authors = {};
/* Installation status of the mod */
- ModStatus status;
+ ModStatus status = ModStatus::Unknown;
/* Metadata information, if any */
- std::shared_ptr<Metadata::ModStruct> metadata;
+ std::shared_ptr<Metadata::ModStruct> metadata = nullptr;
+
+ ModDetails() = default;
+
+ /** Metadata should be handled manually to properly set the mod status. */
+ ModDetails(ModDetails& other)
+ : mod_id(other.mod_id)
+ , name(other.name)
+ , version(other.version)
+ , mcversion(other.mcversion)
+ , homeurl(other.homeurl)
+ , description(other.description)
+ , authors(other.authors)
+ , status(other.status)
+ {}
+
+ ModDetails& operator=(ModDetails& other)
+ {
+ this->mod_id = other.mod_id;
+ this->name = other.name;
+ this->version = other.version;
+ this->mcversion = other.mcversion;
+ this->homeurl = other.homeurl;
+ this->description = other.description;
+ this->authors = other.authors;
+ this->status = other.status;
+
+ return *this;
+ }
+
+ ModDetails& operator=(ModDetails&& other)
+ {
+ this->mod_id = other.mod_id;
+ this->name = other.name;
+ this->version = other.version;
+ this->mcversion = other.mcversion;
+ this->homeurl = other.homeurl;
+ this->description = other.description;
+ this->authors = other.authors;
+ this->status = other.status;
+
+ return *this;
+ }
};
diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp
index 112d219e..4e264a74 100644
--- a/launcher/minecraft/mod/ModFolderModel.cpp
+++ b/launcher/minecraft/mod/ModFolderModel.cpp
@@ -49,428 +49,53 @@
#include "minecraft/mod/tasks/LocalModParseTask.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
-ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : QAbstractListModel(), m_dir(dir), m_is_indexed(is_indexed)
+ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(QDir(dir)), m_is_indexed(is_indexed)
{
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);
- m_watcher = new QFileSystemWatcher(this);
- connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString)));
-}
-
-void ModFolderModel::startWatching()
-{
- if(is_watching)
- return;
-
- update();
-
- // Watch the mods folder
- is_watching = m_watcher->addPath(m_dir.absolutePath());
- if (is_watching) {
- qDebug() << "Started watching " << m_dir.absolutePath();
- } else {
- qDebug() << "Failed to start watching " << m_dir.absolutePath();
- }
-
- // Watch the mods index folder
- is_watching = m_watcher->addPath(indexDir().absolutePath());
- if (is_watching) {
- qDebug() << "Started watching " << indexDir().absolutePath();
- } else {
- qDebug() << "Failed to start watching " << indexDir().absolutePath();
- }
-}
-
-void ModFolderModel::stopWatching()
-{
- if(!is_watching)
- return;
-
- is_watching = !m_watcher->removePath(m_dir.absolutePath());
- if (!is_watching) {
- qDebug() << "Stopped watching " << m_dir.absolutePath();
- } else {
- qDebug() << "Failed to stop watching " << m_dir.absolutePath();
- }
-
- is_watching = !m_watcher->removePath(indexDir().absolutePath());
- if (!is_watching) {
- qDebug() << "Stopped watching " << indexDir().absolutePath();
- } else {
- qDebug() << "Failed to stop watching " << indexDir().absolutePath();
- }
-}
-
-bool ModFolderModel::update()
-{
- if (!isValid()) {
- return false;
- }
- if(m_update) {
- scheduled_update = true;
- return true;
- }
-
- auto index_dir = indexDir();
- auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed);
-
- m_update = task->result();
-
- QThreadPool *threadPool = QThreadPool::globalInstance();
- connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate);
-
- threadPool->start(task);
- return true;
-}
-
-void ModFolderModel::finishUpdate()
-{
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- auto currentList = modsIndex.keys();
- QSet<QString> currentSet(currentList.begin(), currentList.end());
- auto & newMods = m_update->mods;
- auto newList = newMods.keys();
- QSet<QString> newSet(newList.begin(), newList.end());
-#else
- QSet<QString> currentSet = modsIndex.keys().toSet();
- auto& newMods = m_update->mods;
- QSet<QString> newSet = newMods.keys().toSet();
-#endif
-
- // see if the kept mods changed in some way
- {
- QSet<QString> kept = currentSet;
- kept.intersect(newSet);
- for(auto& keptMod : kept) {
- auto newMod = newMods[keptMod];
- auto row = modsIndex[keptMod];
- auto currentMod = mods[row];
- if(newMod->dateTimeChanged() == currentMod->dateTimeChanged()) {
- // no significant change, ignore...
- continue;
- }
- auto oldMod = mods[row];
- if(oldMod->isResolving()) {
- activeTickets.remove(oldMod->resolutionTicket());
- }
-
- mods[row] = newMod;
- resolveMod(mods[row]);
- emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
- }
- }
-
- // remove mods no longer present
- {
- QSet<QString> removed = currentSet;
- QList<int> removedRows;
- removed.subtract(newSet);
- for(auto & removedMod: removed) {
- removedRows.append(modsIndex[removedMod]);
- }
- std::sort(removedRows.begin(), removedRows.end(), std::greater<int>());
- for(auto iter = removedRows.begin(); iter != removedRows.end(); iter++) {
- int removedIndex = *iter;
- beginRemoveRows(QModelIndex(), removedIndex, removedIndex);
- auto removedIter = mods.begin() + removedIndex;
- if((*removedIter)->isResolving()) {
- activeTickets.remove((*removedIter)->resolutionTicket());
- }
-
- mods.erase(removedIter);
- endRemoveRows();
- }
- }
-
- // add new mods to the end
- {
- QSet<QString> added = newSet;
- added.subtract(currentSet);
-
- // When you have a Qt build with assertions turned on, proceeding here will abort the application
- if (added.size() > 0) {
- beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1);
- for (auto& addedMod : added) {
- mods.append(newMods[addedMod]);
- resolveMod(mods.last());
- }
- endInsertRows();
- }
- }
-
- // update index
- {
- modsIndex.clear();
- int idx = 0;
- for(auto mod: mods) {
- modsIndex[mod->internal_id()] = idx;
- idx++;
- }
- }
-
- m_update.reset();
-
- emit updateFinished();
-
- if(scheduled_update) {
- scheduled_update = false;
- update();
- }
-}
-
-void ModFolderModel::resolveMod(Mod::Ptr m)
-{
- if(!m->shouldResolve()) {
- return;
- }
-
- auto task = new LocalModParseTask(nextResolutionTicket, m->type(), m->fileinfo());
- auto result = task->result();
- result->id = m->internal_id();
- activeTickets.insert(nextResolutionTicket, result);
- m->setResolving(true, nextResolutionTicket);
- nextResolutionTicket++;
- QThreadPool *threadPool = QThreadPool::globalInstance();
- connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse);
- threadPool->start(task);
-}
-
-void ModFolderModel::finishModParse(int token)
-{
- auto iter = activeTickets.find(token);
- if(iter == activeTickets.end()) {
- return;
- }
- auto result = *iter;
- activeTickets.remove(token);
- int row = modsIndex[result->id];
- auto mod = mods[row];
- mod->finishResolvingWithDetails(result->details);
- emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
-}
-
-void ModFolderModel::disableInteraction(bool disabled)
-{
- if (interaction_disabled == disabled) {
- return;
- }
- interaction_disabled = disabled;
- if(size()) {
- emit dataChanged(index(0), index(size() - 1));
- }
-}
-
-void ModFolderModel::directoryChanged(QString path)
-{
- update();
-}
-
-bool ModFolderModel::isValid()
-{
- return m_dir.exists() && m_dir.isReadable();
-}
-
-auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>
-{
- QList<Mod::Ptr> selected_mods;
- for (auto i : indexes) {
- if(i.column() != 0)
- continue;
-
- selected_mods.push_back(mods[i.row()]);
- }
- return selected_mods;
-}
-
-// FIXME: this does not take disabled mod (with extra .disable extension) into account...
-bool ModFolderModel::installMod(const QString &filename)
-{
- if(interaction_disabled) {
- return false;
- }
-
- // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
- auto originalPath = FS::NormalizePath(filename);
- QFileInfo fileinfo(originalPath);
-
- if (!fileinfo.exists() || !fileinfo.isReadable())
- {
- qWarning() << "Caught attempt to install non-existing file or file-like object:" << originalPath;
- return false;
- }
- qDebug() << "installing: " << fileinfo.absoluteFilePath();
-
- Mod installedMod(fileinfo);
- if (!installedMod.valid())
- {
- qDebug() << originalPath << "is not a valid mod. Ignoring it.";
- return false;
- }
-
- auto type = installedMod.type();
- if (type == Mod::MOD_UNKNOWN)
- {
- qDebug() << "Cannot recognize mod type of" << originalPath << ", ignoring it.";
- return false;
- }
-
- auto newpath = FS::NormalizePath(FS::PathCombine(m_dir.path(), fileinfo.fileName()));
- if(originalPath == newpath)
- {
- qDebug() << "Overwriting the mod (" << originalPath << ") with itself makes no sense...";
- return false;
- }
-
- if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD)
- {
- if(QFile::exists(newpath) || QFile::exists(newpath + QString(".disabled")))
- {
- if(!QFile::remove(newpath))
- {
- // FIXME: report error in a user-visible way
- qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
- return false;
- }
- qDebug() << newpath << "has been deleted.";
- }
- if (!QFile::copy(fileinfo.filePath(), newpath))
- {
- qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
- // FIXME: report error in a user-visible way
- return false;
- }
- FS::updateTimestamp(newpath);
- QFileInfo newpathInfo(newpath);
- installedMod.repath(newpathInfo);
- update();
- return true;
- }
- else if (type == Mod::MOD_FOLDER)
- {
- QString from = fileinfo.filePath();
- if(QFile::exists(newpath))
- {
- qDebug() << "Ignoring folder " << from << ", it would merge with " << newpath;
- return false;
- }
-
- if (!FS::copy(from, newpath)())
- {
- qWarning() << "Copy of folder from" << originalPath << "to" << newpath << "has (potentially partially) failed.";
- return false;
- }
- QFileInfo newpathInfo(newpath);
- installedMod.repath(newpathInfo);
- update();
- return true;
- }
- return false;
-}
-
-bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
-{
-
- for(auto mod : allMods()){
- if(mod->fileinfo().fileName() == filename){
- auto index_dir = indexDir();
- mod->destroy(index_dir, preserve_metadata);
- return true;
- }
- }
-
- return false;
-}
-
-bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable)
-{
- if(interaction_disabled) {
- return false;
- }
-
- if(indexes.isEmpty())
- return true;
-
- for (auto index: indexes)
- {
- if(index.column() != 0) {
- continue;
- }
- setModStatus(index.row(), enable);
- }
- return true;
-}
-
-bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
-{
- if(interaction_disabled) {
- return false;
- }
-
- if(indexes.isEmpty())
- return true;
-
- for (auto i: indexes)
- {
- if(i.column() != 0) {
- continue;
- }
- auto m = mods[i.row()];
- auto index_dir = indexDir();
- m->destroy(index_dir);
- }
- return true;
-}
-
-int ModFolderModel::columnCount(const QModelIndex &parent) const
-{
- return NUM_COLUMNS;
+ m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE };
}
QVariant ModFolderModel::data(const QModelIndex &index, int role) const
{
- if (!index.isValid())
- return QVariant();
+ if (!validateIndex(index))
+ return {};
int row = index.row();
int column = index.column();
- if (row < 0 || row >= mods.size())
- return QVariant();
-
switch (role)
{
case Qt::DisplayRole:
switch (column)
{
case NameColumn:
- return mods[row]->name();
+ return m_resources[row]->name();
case VersionColumn: {
- switch(mods[row]->type()) {
- case Mod::MOD_FOLDER:
+ switch(m_resources[row]->type()) {
+ case ResourceType::FOLDER:
return tr("Folder");
- case Mod::MOD_SINGLEFILE:
+ case ResourceType::SINGLEFILE:
return tr("File");
default:
break;
}
- return mods[row]->version();
+ return at(row)->version();
}
case DateColumn:
- return mods[row]->dateTimeChanged();
+ return m_resources[row]->dateTimeChanged();
default:
return QVariant();
}
case Qt::ToolTipRole:
- return mods[row]->internal_id();
+ return m_resources[row]->internal_id();
case Qt::CheckStateRole:
switch (column)
{
case ActiveColumn:
- return mods[row]->enabled() ? Qt::Checked : Qt::Unchecked;
+ return at(row)->enabled() ? Qt::Checked : Qt::Unchecked;
default:
return QVariant();
}
@@ -479,61 +104,6 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const
}
}
-bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, int role)
-{
- if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
- {
- return false;
- }
-
- if (role == Qt::CheckStateRole)
- {
- return setModStatus(index.row(), Toggle);
- }
- return false;
-}
-
-bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action)
-{
- if(row < 0 || row >= mods.size()) {
- return false;
- }
-
- auto &mod = mods[row];
- bool desiredStatus;
- switch(action) {
- case Enable:
- desiredStatus = true;
- break;
- case Disable:
- desiredStatus = false;
- break;
- case Toggle:
- default:
- desiredStatus = !mod->enabled();
- break;
- }
-
- if(desiredStatus == mod->enabled()) {
- return true;
- }
-
- // preserve the row, but change its ID
- auto oldId = mod->internal_id();
- if(!mod->enable(!mod->enabled())) {
- return false;
- }
- 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?
- }
- modsIndex.remove(oldId);
- modsIndex[newId] = row;
- emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
- return true;
-}
-
QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
{
switch (role)
@@ -573,65 +143,151 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in
return QVariant();
}
-Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const
+int ModFolderModel::columnCount(const QModelIndex &parent) const
+{
+ return NUM_COLUMNS;
+}
+
+Task* ModFolderModel::createUpdateTask()
+{
+ auto index_dir = indexDir();
+ auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load, this);
+ m_first_folder_load = false;
+ return task;
+}
+
+Task* ModFolderModel::createParseTask(Resource const& resource)
+{
+ return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo());
+}
+
+bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
+{
+ for(auto mod : allMods()){
+ if(mod->fileinfo().fileName() == filename){
+ auto index_dir = indexDir();
+ mod->destroy(index_dir, preserve_metadata);
+
+ update();
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
{
- Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
- auto flags = defaultFlags;
- if(interaction_disabled) {
- flags &= ~Qt::ItemIsDropEnabled;
+ if(!m_can_interact) {
+ return false;
}
- else
+
+ if(indexes.isEmpty())
+ return true;
+
+ for (auto i: indexes)
{
- flags |= Qt::ItemIsDropEnabled;
- if(index.isValid()) {
- flags |= Qt::ItemIsUserCheckable;
+ if(i.column() != 0) {
+ continue;
}
+ auto m = at(i.row());
+ auto index_dir = indexDir();
+ m->destroy(index_dir);
}
- return flags;
+
+ update();
+
+ return true;
}
-Qt::DropActions ModFolderModel::supportedDropActions() const
+bool ModFolderModel::isValid()
{
- // copy from outside, move from within and other mod lists
- return Qt::CopyAction | Qt::MoveAction;
+ return m_dir.exists() && m_dir.isReadable();
}
-QStringList ModFolderModel::mimeTypes() const
+bool ModFolderModel::startWatching()
{
- QStringList types;
- types << "text/uri-list";
- return types;
+ // Remove orphaned metadata next time
+ m_first_folder_load = true;
+ return ResourceFolderModel::startWatching({ m_dir.absolutePath(), indexDir().absolutePath() });
}
-bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
+bool ModFolderModel::stopWatching()
{
- if (action == Qt::IgnoreAction)
- {
- return true;
+ return ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() });
+}
+
+auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod*>
+{
+ QList<Mod*> selected_resources;
+ for (auto i : indexes) {
+ if(i.column() != 0)
+ continue;
+
+ selected_resources.push_back(at(i.row()));
}
+ return selected_resources;
+}
- // check if the action is supported
- if (!data || !(action & supportedDropActions()))
- {
- return false;
+auto ModFolderModel::allMods() -> QList<Mod*>
+{
+ QList<Mod*> mods;
+
+ for (auto& res : m_resources) {
+ mods.append(static_cast<Mod*>(res.get()));
}
- // files dropped from outside?
- if (data->hasUrls())
- {
- auto urls = data->urls();
- for (auto url : urls)
- {
- // only local files may be dropped...
- if (!url.isLocalFile())
- {
- continue;
- }
- // TODO: implement not only copy, but also move
- // FIXME: handle errors here
- installMod(url.toLocalFile());
- }
- return true;
+ return mods;
+}
+
+void ModFolderModel::onUpdateSucceeded()
+{
+ auto update_results = static_cast<ModFolderLoadTask*>(m_current_update_task.get())->result();
+
+ auto& new_mods = update_results->mods;
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ auto current_list = m_resources_index.keys();
+ QSet<QString> current_set(current_list.begin(), current_list.end());
+
+ auto new_list = new_mods.keys();
+ QSet<QString> new_set(new_list.begin(), new_list.end());
+#else
+ QSet<QString> current_set(m_resources_index.keys().toSet());
+ QSet<QString> new_set(new_mods.keys().toSet());
+#endif
+
+ applyUpdates(current_set, new_set, new_mods);
+
+ m_current_update_task.reset();
+
+ if (m_scheduled_update) {
+ m_scheduled_update = false;
+ update();
+ } else {
+ emit updateFinished();
}
- return false;
+}
+
+void ModFolderModel::onParseSucceeded(int ticket, QString mod_id)
+{
+ auto iter = m_active_parse_tasks.constFind(ticket);
+ if (iter == m_active_parse_tasks.constEnd())
+ return;
+
+ int row = m_resources_index[mod_id];
+
+ auto parse_task = *iter;
+ auto cast_task = static_cast<LocalModParseTask*>(parse_task.get());
+
+ Q_ASSERT(cast_task->token() == ticket);
+
+ auto resource = find(mod_id);
+
+ auto result = cast_task->result();
+ if (result && resource)
+ resource->finishResolvingWithDetails(std::move(result->details));
+
+ emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
}
diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h
index a7d3ece0..c33195ed 100644
--- a/launcher/minecraft/mod/ModFolderModel.h
+++ b/launcher/minecraft/mod/ModFolderModel.h
@@ -44,6 +44,7 @@
#include <QAbstractListModel>
#include "Mod.h"
+#include "ResourceFolderModel.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalModParseTask.h"
@@ -56,7 +57,7 @@ class QFileSystemWatcher;
* A legacy mod list.
* Backed by a folder.
*/
-class ModFolderModel : public QAbstractListModel
+class ModFolderModel : public ResourceFolderModel
{
Q_OBJECT
public:
@@ -75,105 +76,38 @@ public:
};
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;
- Qt::DropActions supportedDropActions() const override;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
- /// flags, mostly to support drag&drop
- virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
- QStringList mimeTypes() const override;
- bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+ int columnCount(const QModelIndex &parent) const override;
- virtual int rowCount(const QModelIndex &) const override
- {
- return size();
- }
-
- virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
- virtual int columnCount(const QModelIndex &parent) const override;
-
- size_t size() const
- {
- return mods.size();
- }
- ;
- bool empty() const
- {
- return size() == 0;
- }
- Mod& operator[](size_t index)
- {
- return *mods[index];
- }
- const Mod& at(size_t index) const
- {
- return *mods.at(index);
- }
-
- /// Reloads the mod list and returns true if the list changed.
- bool update();
-
- /**
- * Adds the given mod to the list at the given index - if the list supports custom ordering
- */
- bool installMod(const QString& filename);
+ [[nodiscard]] Task* createUpdateTask() override;
+ [[nodiscard]] Task* createParseTask(Resource const&) override;
+ bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); }
bool uninstallMod(const QString& filename, bool preserve_metadata = false);
/// Deletes all the selected mods
bool deleteMods(const QModelIndexList &indexes);
- /// Enable or disable listed mods
- bool setModStatus(const QModelIndexList &indexes, ModStatusAction action);
-
- void startWatching();
- void stopWatching();
-
bool isValid();
- QDir& dir()
- {
- return m_dir;
- }
-
- QDir indexDir()
- {
- return { QString("%1/.index").arg(dir().absolutePath()) };
- }
+ bool startWatching() override;
+ bool stopWatching() override;
- const QList<Mod::Ptr>& allMods()
- {
- return mods;
- }
+ QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; }
- auto selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>;
+ auto selectedMods(QModelIndexList& indexes) -> QList<Mod*>;
+ auto allMods() -> QList<Mod*>;
-public slots:
- void disableInteraction(bool disabled);
+ RESOURCE_HELPERS(Mod)
private
slots:
- void directoryChanged(QString path);
- void finishUpdate();
- void finishModParse(int token);
-
-signals:
- void updateFinished();
-
-private:
- void resolveMod(Mod::Ptr m);
- bool setModStatus(int index, ModStatusAction action);
+ void onUpdateSucceeded() override;
+ void onParseSucceeded(int ticket, QString resource_id) override;
protected:
- QFileSystemWatcher *m_watcher;
- bool is_watching = false;
- ModFolderLoadTask::ResultPtr m_update;
- bool scheduled_update = false;
- bool interaction_disabled = false;
- QDir m_dir;
bool m_is_indexed;
- QMap<QString, int> modsIndex;
- QMap<int, LocalModParseTask::ResultPtr> activeTickets;
- int nextResolutionTicket = 0;
- QList<Mod::Ptr> mods;
+ bool m_first_folder_load = true;
};
diff --git a/launcher/minecraft/mod/ModFolderModel_test.cpp b/launcher/minecraft/mod/ModFolderModel_test.cpp
deleted file mode 100644
index b4d37ce5..00000000
--- a/launcher/minecraft/mod/ModFolderModel_test.cpp
+++ /dev/null
@@ -1,92 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
-* PolyMC - Minecraft Launcher
-* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
-*
-* This program is free software: you can 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 <https://www.gnu.org/licenses/>.
-*
-* 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 <QTest>
-#include <QTemporaryDir>
-
-#include "FileSystem.h"
-#include "minecraft/mod/ModFolderModel.h"
-
-class ModFolderModelTest : public QObject
-{
- Q_OBJECT
-
-private
-slots:
- // test for GH-1178 - install a folder with files to a mod list
- void test_1178()
- {
- // source
- QString source = QFINDTESTDATA("testdata/test_folder");
-
- // sanity check
- QVERIFY(!source.endsWith('/'));
-
- auto verify = [](QString path)
- {
- QDir target_dir(FS::PathCombine(path, "test_folder"));
- QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
- QVERIFY(target_dir.entryList().contains("assets"));
- };
-
- // 1. test with no trailing /
- {
- 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());
- }
-
- // 2. test with trailing /
- {
- 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());
- }
- }
-};
-
-QTEST_GUILESS_MAIN(ModFolderModelTest)
-
-#include "ModFolderModel_test.moc"
diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp
new file mode 100644
index 00000000..0fbcfd7c
--- /dev/null
+++ b/launcher/minecraft/mod/Resource.cpp
@@ -0,0 +1,147 @@
+#include "Resource.h"
+
+#include <QRegularExpression>
+
+#include "FileSystem.h"
+
+Resource::Resource(QObject* parent) : QObject(parent) {}
+
+Resource::Resource(QFileInfo file_info) : QObject()
+{
+ setFile(file_info);
+}
+
+void Resource::setFile(QFileInfo file_info)
+{
+ m_file_info = file_info;
+ parseFile();
+}
+
+void Resource::parseFile()
+{
+ QString file_name{ m_file_info.fileName() };
+
+ m_type = ResourceType::UNKNOWN;
+
+ m_internal_id = file_name;
+
+ if (m_file_info.isDir()) {
+ m_type = ResourceType::FOLDER;
+ m_name = file_name;
+ } else if (m_file_info.isFile()) {
+ if (file_name.endsWith(".disabled")) {
+ file_name.chop(9);
+ m_enabled = false;
+ }
+
+ if (file_name.endsWith(".zip") || file_name.endsWith(".jar")) {
+ m_type = ResourceType::ZIPFILE;
+ file_name.chop(4);
+ } else if (file_name.endsWith(".litemod")) {
+ m_type = ResourceType::LITEMOD;
+ file_name.chop(8);
+ } else {
+ m_type = ResourceType::SINGLEFILE;
+ }
+
+ m_name = file_name;
+ }
+
+ m_changed_date_time = m_file_info.lastModified();
+}
+
+static void removeThePrefix(QString& string)
+{
+ QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption);
+ string.remove(regex);
+ string = string.trimmed();
+}
+
+std::pair<int, bool> Resource::compare(const Resource& other, SortType type) const
+{
+ switch (type) {
+ default:
+ case SortType::ENABLED:
+ if (enabled() && !other.enabled())
+ return { 1, type == SortType::ENABLED };
+ if (!enabled() && other.enabled())
+ return { -1, type == SortType::ENABLED };
+ case SortType::NAME: {
+ QString this_name{ name() };
+ QString other_name{ other.name() };
+
+ removeThePrefix(this_name);
+ removeThePrefix(other_name);
+
+ auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive);
+ if (compare_result != 0)
+ return { compare_result, type == SortType::NAME };
+ }
+ case SortType::DATE:
+ if (dateTimeChanged() > other.dateTimeChanged())
+ return { 1, type == SortType::DATE };
+ if (dateTimeChanged() < other.dateTimeChanged())
+ return { -1, type == SortType::DATE };
+ }
+
+ return { 0, false };
+}
+
+bool Resource::applyFilter(QRegularExpression filter) const
+{
+ return filter.match(name()).hasMatch();
+}
+
+bool Resource::enable(EnableAction action)
+{
+ if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER)
+ return false;
+
+
+ QString path = m_file_info.absoluteFilePath();
+ QFile file(path);
+
+ bool enable = true;
+ switch (action) {
+ case EnableAction::ENABLE:
+ enable = true;
+ break;
+ case EnableAction::DISABLE:
+ enable = false;
+ break;
+ case EnableAction::TOGGLE:
+ default:
+ enable = !enabled();
+ break;
+ }
+
+ if (m_enabled == enable)
+ return false;
+
+ if (enable) {
+ // m_enabled is false, but there's no '.disabled' suffix.
+ // TODO: Report error?
+ if (!path.endsWith(".disabled"))
+ return false;
+ path.chop(9);
+
+ if (!file.rename(path))
+ return false;
+ } else {
+ path += ".disabled";
+
+ if (!file.rename(path))
+ return false;
+ }
+
+ setFile(QFileInfo(path));
+
+ m_enabled = enable;
+ return true;
+}
+
+bool Resource::destroy()
+{
+ m_type = ResourceType::UNKNOWN;
+ return FS::deletePath(m_file_info.filePath());
+}
diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h
new file mode 100644
index 00000000..cee1f172
--- /dev/null
+++ b/launcher/minecraft/mod/Resource.h
@@ -0,0 +1,115 @@
+#pragma once
+
+#include <QDateTime>
+#include <QFileInfo>
+#include <QObject>
+#include <QPointer>
+
+#include "QObjectPtr.h"
+
+enum class ResourceType {
+ UNKNOWN, //!< Indicates an unspecified resource type.
+ ZIPFILE, //!< The resource is a zip file containing the resource's class files.
+ SINGLEFILE, //!< The resource is a single file (not a zip file).
+ FOLDER, //!< The resource is in a folder on the filesystem.
+ LITEMOD, //!< The resource is a litemod
+};
+
+enum class SortType {
+ NAME,
+ DATE,
+ VERSION,
+ ENABLED,
+};
+
+enum class EnableAction {
+ ENABLE,
+ DISABLE,
+ TOGGLE
+};
+
+/** General class for managed resources. It mirrors a file in disk, with some more info
+ * for display and house-keeping purposes.
+ *
+ * Subclass it to add additional data / behavior, such as Mods or Resource packs.
+ */
+class Resource : public QObject {
+ Q_OBJECT
+ Q_DISABLE_COPY(Resource)
+ public:
+ using Ptr = shared_qobject_ptr<Resource>;
+ using WeakPtr = QPointer<Resource>;
+
+ Resource(QObject* parent = nullptr);
+ Resource(QFileInfo file_info);
+ Resource(QString file_path) : Resource(QFileInfo(file_path)) {}
+
+ ~Resource() override = default;
+
+ void setFile(QFileInfo file_info);
+ void parseFile();
+
+ [[nodiscard]] auto fileinfo() const -> QFileInfo { return m_file_info; }
+ [[nodiscard]] auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; }
+ [[nodiscard]] auto internal_id() const -> QString { return m_internal_id; }
+ [[nodiscard]] auto type() const -> ResourceType { return m_type; }
+ [[nodiscard]] bool enabled() const { return m_enabled; }
+
+ [[nodiscard]] virtual auto name() const -> QString { return m_name; }
+ [[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; }
+
+ /** Compares two Resources, for sorting purposes, considering a ascending order, returning:
+ * > 0: 'this' comes after 'other'
+ * = 0: 'this' is equal to 'other'
+ * < 0: 'this' comes before 'other'
+ *
+ * The second argument in the pair is true if the sorting type that decided which one is greater was 'type'.
+ */
+ [[nodiscard]] virtual auto compare(Resource const& other, SortType type = SortType::NAME) const -> std::pair<int, bool>;
+
+ /** Returns whether the given filter should filter out 'this' (false),
+ * or if such filter includes the Resource (true).
+ */
+ [[nodiscard]] virtual bool applyFilter(QRegularExpression filter) const;
+
+ /** Changes the enabled property, according to 'action'.
+ *
+ * Returns whether a change was applied to the Resource's properties.
+ */
+ bool enable(EnableAction action);
+
+ [[nodiscard]] auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; }
+ [[nodiscard]] auto isResolving() const -> bool { return m_is_resolving; }
+ [[nodiscard]] auto resolutionTicket() const -> int { return m_resolution_ticket; }
+
+ void setResolving(bool resolving, int resolutionTicket)
+ {
+ m_is_resolving = resolving;
+ m_resolution_ticket = resolutionTicket;
+ }
+
+ // Delete all files of this resource.
+ bool destroy();
+
+ protected:
+ /* The file corresponding to this resource. */
+ QFileInfo m_file_info;
+ /* The cached date when this file was last changed. */
+ QDateTime m_changed_date_time;
+
+ /* Internal ID for internal purposes. Properties such as human-readability should not be assumed. */
+ QString m_internal_id;
+ /* Name as reported via the file name. In the absence of a better name, this is shown to the user. */
+ QString m_name;
+
+ /* The type of file we're dealing with. */
+ ResourceType m_type = ResourceType::UNKNOWN;
+
+ /* Whether the resource is enabled (e.g. shows up in the game) or not. */
+ bool m_enabled = true;
+
+ /* Used to keep trach of pending / concluded actions on the resource. */
+ bool m_is_resolving = false;
+ bool m_is_resolved = false;
+ int m_resolution_ticket = 0;
+};
diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp
new file mode 100644
index 00000000..bc18ddc2
--- /dev/null
+++ b/launcher/minecraft/mod/ResourceFolderModel.cpp
@@ -0,0 +1,522 @@
+#include "ResourceFolderModel.h"
+
+#include <QDebug>
+#include <QMimeData>
+#include <QThreadPool>
+#include <QUrl>
+
+#include "FileSystem.h"
+
+#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
+
+#include "tasks/Task.h"
+
+ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractListModel(parent), m_dir(dir), m_watcher(this)
+{
+ m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
+ m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
+
+ connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged);
+}
+
+bool ResourceFolderModel::startWatching(const QStringList paths)
+{
+ if (m_is_watching)
+ return false;
+
+ auto couldnt_be_watched = m_watcher.addPaths(paths);
+ for (auto path : paths) {
+ if (couldnt_be_watched.contains(path))
+ qDebug() << "Failed to start watching " << path;
+ else
+ qDebug() << "Started watching " << path;
+ }
+
+ update();
+
+ m_is_watching = !m_is_watching;
+ return m_is_watching;
+}
+
+bool ResourceFolderModel::stopWatching(const QStringList paths)
+{
+ if (!m_is_watching)
+ return false;
+
+ auto couldnt_be_stopped = m_watcher.removePaths(paths);
+ for (auto path : paths) {
+ if (couldnt_be_stopped.contains(path))
+ qDebug() << "Failed to stop watching " << path;
+ else
+ qDebug() << "Stopped watching " << path;
+ }
+
+ m_is_watching = !m_is_watching;
+ return !m_is_watching;
+}
+
+bool ResourceFolderModel::installResource(QString original_path)
+{
+ if (!m_can_interact) {
+ return false;
+ }
+
+ // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
+ original_path = FS::NormalizePath(original_path);
+ QFileInfo file_info(original_path);
+
+ if (!file_info.exists() || !file_info.isReadable()) {
+ qWarning() << "Caught attempt to install non-existing file or file-like object:" << original_path;
+ return false;
+ }
+ qDebug() << "Installing: " << file_info.absoluteFilePath();
+
+ Resource resource(file_info);
+ if (!resource.valid()) {
+ qWarning() << original_path << "is not a valid resource. Ignoring it.";
+ return false;
+ }
+
+ auto new_path = FS::NormalizePath(m_dir.filePath(file_info.fileName()));
+ if (original_path == new_path) {
+ qWarning() << "Overwriting the mod (" << original_path << ") with itself makes no sense...";
+ return false;
+ }
+
+ switch (resource.type()) {
+ case ResourceType::SINGLEFILE:
+ case ResourceType::ZIPFILE:
+ case ResourceType::LITEMOD: {
+ if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) {
+ if (!QFile::remove(new_path)) {
+ qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!";
+ return false;
+ }
+ qDebug() << new_path << "has been deleted.";
+ }
+
+ if (!QFile::copy(original_path, new_path)) {
+ qCritical() << "Copy from" << original_path << "to" << new_path << "has failed.";
+ return false;
+ }
+
+ FS::updateTimestamp(new_path);
+
+ QFileInfo new_path_file_info(new_path);
+ resource.setFile(new_path_file_info);
+
+ if (!m_is_watching)
+ return update();
+
+ return true;
+ }
+ case ResourceType::FOLDER: {
+ if (QFile::exists(new_path)) {
+ qDebug() << "Ignoring folder '" << original_path << "', it would merge with" << new_path;
+ return false;
+ }
+
+ if (!FS::copy(original_path, new_path)()) {
+ qWarning() << "Copy of folder from" << original_path << "to" << new_path << "has (potentially partially) failed.";
+ return false;
+ }
+
+ QFileInfo newpathInfo(new_path);
+ resource.setFile(newpathInfo);
+
+ if (!m_is_watching)
+ return update();
+
+ return true;
+ }
+ default:
+ break;
+ }
+ return false;
+}
+
+bool ResourceFolderModel::uninstallResource(QString file_name)
+{
+ for (auto& resource : m_resources) {
+ if (resource->fileinfo().fileName() == file_name) {
+ auto res = resource->destroy();
+
+ update();
+
+ return res;
+ }
+ }
+ return false;
+}
+
+bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
+{
+ if (!m_can_interact)
+ return false;
+
+ if (indexes.isEmpty())
+ return true;
+
+ for (auto i : indexes) {
+ if (i.column() != 0) {
+ continue;
+ }
+
+ auto& resource = m_resources.at(i.row());
+
+ resource->destroy();
+ }
+
+ update();
+
+ return true;
+}
+
+bool ResourceFolderModel::setResourceEnabled(const QModelIndexList &indexes, EnableAction action)
+{
+ if (!m_can_interact)
+ return false;
+
+ if (indexes.isEmpty())
+ return true;
+
+ bool succeeded = true;
+ for (auto const& idx : indexes) {
+ if (!validateIndex(idx) || idx.column() != 0)
+ continue;
+
+ int row = idx.row();
+
+ auto& resource = m_resources[row];
+
+ // Preserve the row, but change its ID
+ auto old_id = resource->internal_id();
+ if (!resource->enable(action)) {
+ succeeded = false;
+ continue;
+ }
+
+ auto new_id = resource->internal_id();
+ if (m_resources_index.contains(new_id)) {
+ // FIXME: https://github.com/PolyMC/PolyMC/issues/550
+ }
+
+ m_resources_index.remove(old_id);
+ m_resources_index[new_id] = row;
+
+ emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
+ }
+
+ return succeeded;
+}
+
+static QMutex s_update_task_mutex;
+bool ResourceFolderModel::update()
+{
+ // We hold a lock here to prevent race conditions on the m_current_update_task reset.
+ QMutexLocker lock(&s_update_task_mutex);
+
+ // Already updating, so we schedule a future update and return.
+ if (m_current_update_task) {
+ m_scheduled_update = true;
+ return false;
+ }
+
+ m_current_update_task.reset(createUpdateTask());
+ if (!m_current_update_task)
+ return false;
+
+ connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded,
+ Qt::ConnectionType::QueuedConnection);
+ connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection);
+
+ auto* thread_pool = QThreadPool::globalInstance();
+ thread_pool->start(m_current_update_task.get());
+
+ return true;
+}
+
+void ResourceFolderModel::resolveResource(Resource::Ptr res)
+{
+ if (!res->shouldResolve()) {
+ return;
+ }
+
+ auto task = createParseTask(*res);
+ if (!task)
+ return;
+
+ m_ticket_mutex.lock();
+ int ticket = m_next_resolution_ticket;
+ m_next_resolution_ticket += 1;
+ m_ticket_mutex.unlock();
+
+ res->setResolving(true, ticket);
+ m_active_parse_tasks.insert(ticket, task);
+
+ connect(
+ task, &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
+ connect(
+ task, &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
+ connect(
+ task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection);
+
+ auto* thread_pool = QThreadPool::globalInstance();
+ thread_pool->start(task);
+}
+
+void ResourceFolderModel::onUpdateSucceeded()
+{
+ auto update_results = static_cast<BasicFolderLoadTask*>(m_current_update_task.get())->result();
+
+ auto& new_resources = update_results->resources;
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ auto current_list = m_resources_index.keys();
+ QSet<QString> current_set(current_list.begin(), current_list.end());
+
+ auto new_list = new_resources.keys();
+ QSet<QString> new_set(new_list.begin(), new_list.end());
+#else
+ QSet<QString> current_set(m_resources_index.keys().toSet());
+ QSet<QString> new_set(new_resources.keys().toSet());
+#endif
+
+ applyUpdates(current_set, new_set, new_resources);
+
+ m_current_update_task.reset();
+
+ if (m_scheduled_update) {
+ m_scheduled_update = false;
+ update();
+ } else {
+ emit updateFinished();
+ }
+}
+
+void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id)
+{
+ auto iter = m_active_parse_tasks.constFind(ticket);
+ if (iter == m_active_parse_tasks.constEnd())
+ return;
+
+ int row = m_resources_index[resource_id];
+ emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
+}
+
+Task* ResourceFolderModel::createUpdateTask()
+{
+ return new BasicFolderLoadTask(m_dir);
+}
+
+bool ResourceFolderModel::hasPendingParseTasks() const
+{
+ return !m_active_parse_tasks.isEmpty();
+}
+
+void ResourceFolderModel::directoryChanged(QString path)
+{
+ update();
+}
+
+Qt::DropActions ResourceFolderModel::supportedDropActions() const
+{
+ // copy from outside, move from within and other resource lists
+ return Qt::CopyAction | Qt::MoveAction;
+}
+
+Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const
+{
+ Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
+ auto flags = defaultFlags;
+ if (!m_can_interact) {
+ flags &= ~Qt::ItemIsDropEnabled;
+ } else {
+ flags |= Qt::ItemIsDropEnabled;
+ if (index.isValid()) {
+ flags |= Qt::ItemIsUserCheckable;
+ }
+ }
+ return flags;
+}
+
+QStringList ResourceFolderModel::mimeTypes() const
+{
+ QStringList types;
+ types << "text/uri-list";
+ return types;
+}
+
+bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
+{
+ if (action == Qt::IgnoreAction) {
+ return true;
+ }
+
+ // check if the action is supported
+ if (!data || !(action & supportedDropActions())) {
+ return false;
+ }
+
+ // files dropped from outside?
+ if (data->hasUrls()) {
+ auto urls = data->urls();
+ for (auto url : urls) {
+ // only local files may be dropped...
+ if (!url.isLocalFile()) {
+ continue;
+ }
+ // TODO: implement not only copy, but also move
+ // FIXME: handle errors here
+ installResource(url.toLocalFile());
+ }
+ return true;
+ }
+ return false;
+}
+
+bool ResourceFolderModel::validateIndex(const QModelIndex& index) const
+{
+ if (!index.isValid())
+ return false;
+
+ int row = index.row();
+ if (row < 0 || row >= m_resources.size())
+ return false;
+
+ return true;
+}
+
+QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
+{
+ if (!validateIndex(index))
+ return {};
+
+ int row = index.row();
+ int column = index.column();
+
+ switch (role) {
+ case Qt::DisplayRole:
+ switch (column) {
+ case NAME_COLUMN:
+ return m_resources[row]->name();
+ case DATE_COLUMN:
+ return m_resources[row]->dateTimeChanged();
+ default:
+ return {};
+ }
+ case Qt::ToolTipRole:
+ return m_resources[row]->internal_id();
+ case Qt::CheckStateRole:
+ switch (column) {
+ case ACTIVE_COLUMN:
+ return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
+ default:
+ return {};
+ }
+ default:
+ return {};
+ }
+}
+
+bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& value, int role)
+{
+ int row = index.row();
+ if (row < 0 || row >= rowCount(index) || !index.isValid())
+ return false;
+
+ if (role == Qt::CheckStateRole)
+ return setResourceEnabled({ index }, EnableAction::TOGGLE);
+
+ return false;
+}
+
+QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role) {
+ case Qt::DisplayRole:
+ switch (section) {
+ case NAME_COLUMN:
+ return tr("Name");
+ case DATE_COLUMN:
+ return tr("Last modified");
+ default:
+ return {};
+ }
+ case Qt::ToolTipRole: {
+ switch (section) {
+ case ACTIVE_COLUMN:
+ //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
+ return tr("Is the resource enabled?");
+ case NAME_COLUMN:
+ //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
+ return tr("The name of the resource.");
+ case DATE_COLUMN:
+ //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
+ return tr("The date and time this resource was last changed (or added).");
+ default:
+ return {};
+ }
+ }
+ default:
+ break;
+ }
+
+ return {};
+}
+
+QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent)
+{
+ return new ProxyModel(parent);
+}
+
+SortType ResourceFolderModel::columnToSortKey(size_t column) const
+{
+ Q_ASSERT(m_column_sort_keys.size() == columnCount());
+ return m_column_sort_keys.at(column);
+}
+
+void ResourceFolderModel::enableInteraction(bool enabled)
+{
+ if (m_can_interact == enabled)
+ return;
+
+ m_can_interact = enabled;
+ if (size())
+ emit dataChanged(index(0), index(size() - 1));
+}
+
+/* Standard Proxy Model for createFilterProxyModel */
+[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
+{
+ auto* model = qobject_cast<ResourceFolderModel*>(sourceModel());
+ if (!model)
+ return true;
+
+ const auto& resource = model->at(source_row);
+
+ return resource.applyFilter(filterRegularExpression());
+}
+
+[[nodiscard]] bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const
+{
+ auto* model = qobject_cast<ResourceFolderModel*>(sourceModel());
+ if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) {
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+ }
+
+ // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and
+ // proceed.
+
+ auto column_sort_key = model->columnToSortKey(source_left.column());
+ auto const& resource_left = model->at(source_left.row());
+ auto const& resource_right = model->at(source_right.row());
+
+ auto compare_result = resource_left.compare(resource_right, column_sort_key);
+ if (compare_result.first == 0)
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+
+ if (compare_result.second || sortOrder() != Qt::DescendingOrder)
+ return (compare_result.first < 0);
+ return (compare_result.first > 0);
+}
diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h
new file mode 100644
index 00000000..e27b5db6
--- /dev/null
+++ b/launcher/minecraft/mod/ResourceFolderModel.h
@@ -0,0 +1,326 @@
+#pragma once
+
+#include <QAbstractListModel>
+#include <QDir>
+#include <QFileSystemWatcher>
+#include <QMutex>
+#include <QSet>
+#include <QSortFilterProxyModel>
+
+#include "Resource.h"
+
+#include "tasks/Task.h"
+
+class QSortFilterProxyModel;
+
+/** A basic model for external resources.
+ *
+ * This model manages a list of resources. As such, external users of such resources do not own them,
+ * and the resource's lifetime is contingent on the model's lifetime.
+ *
+ * TODO: Make the resources unique pointers accessible through weak pointers.
+ */
+class ResourceFolderModel : public QAbstractListModel {
+ Q_OBJECT
+ public:
+ ResourceFolderModel(QDir, QObject* parent = nullptr);
+
+ /** Starts watching the paths for changes.
+ *
+ * Returns whether starting to watch all the paths was successful.
+ * If one or more fails, it returns false.
+ */
+ bool startWatching(const QStringList paths);
+
+ /** Stops watching the paths for changes.
+ *
+ * Returns whether stopping to watch all the paths was successful.
+ * If one or more fails, it returns false.
+ */
+ bool stopWatching(const QStringList paths);
+
+ /* Helper methods for subclasses, using a predetermined list of paths. */
+ virtual bool startWatching() { return startWatching({ m_dir.absolutePath() }); };
+ virtual bool stopWatching() { return stopWatching({ m_dir.absolutePath() }); };
+
+ /** Given a path in the system, install that resource, moving it to its place in the
+ * instance file hierarchy.
+ *
+ * Returns whether the installation was succcessful.
+ */
+ virtual bool installResource(QString path);
+
+ /** Uninstall (i.e. remove all data about it) a resource, given its file name.
+ *
+ * Returns whether the removal was successful.
+ */
+ virtual bool uninstallResource(QString file_name);
+ virtual bool deleteResources(const QModelIndexList&);
+
+ /** Applies the given 'action' to the resources in 'indexes'.
+ *
+ * Returns whether the action was successfully applied to all resources.
+ */
+ virtual bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action);
+
+ /** Creates a new update task and start it. Returns false if no update was done, like when an update is already underway. */
+ virtual bool update();
+
+ /** Creates a new parse task, if needed, for 'res' and start it.*/
+ virtual void resolveResource(Resource::Ptr res);
+
+ [[nodiscard]] size_t size() const { return m_resources.size(); };
+ [[nodiscard]] bool empty() const { return size() == 0; }
+ [[nodiscard]] Resource& at(int index) { return *m_resources.at(index); }
+ [[nodiscard]] Resource const& at(int index) const { return *m_resources.at(index); }
+ [[nodiscard]] QList<Resource::Ptr> const& all() const { return m_resources; }
+
+ [[nodiscard]] QDir const& dir() const { return m_dir; }
+
+ /** Checks whether there's any parse tasks being done.
+ *
+ * Since they can be quite expensive, and are usually done in a separate thread, if we were to destroy the model while having
+ * such tasks would introduce an undefined behavior, most likely resulting in a crash.
+ */
+ [[nodiscard]] bool hasPendingParseTasks() const;
+
+ /* Qt behavior */
+
+ /* Basic columns */
+ enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS };
+
+ [[nodiscard]] int rowCount(const QModelIndex& = {}) const override { return size(); }
+ [[nodiscard]] int columnCount(const QModelIndex& = {}) const override { return NUM_COLUMNS; };
+
+ [[nodiscard]] Qt::DropActions supportedDropActions() const override;
+
+ /// flags, mostly to support drag&drop
+ [[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const override;
+ [[nodiscard]] QStringList mimeTypes() const override;
+ bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override;
+
+ [[nodiscard]] bool validateIndex(const QModelIndex& index) const;
+
+ [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
+
+ [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+
+ /** This creates a proxy model to filter / sort the model for a UI.
+ *
+ * The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead!
+ */
+ QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr);
+
+ [[nodiscard]] SortType columnToSortKey(size_t column) const;
+
+ class ProxyModel : public QSortFilterProxyModel {
+ public:
+ explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {}
+
+ protected:
+ [[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
+ [[nodiscard]] bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override;
+ };
+
+ public slots:
+ void enableInteraction(bool enabled);
+ void disableInteraction(bool disabled) { enableInteraction(!disabled); }
+
+ signals:
+ void updateFinished();
+
+ protected:
+ /** This creates a new update task to be executed by update().
+ *
+ * The task should load and parse all resources necessary, and provide a way of accessing such results.
+ *
+ * This Task is normally executed when opening a page, so it shouldn't contain much heavy work.
+ * If such work is needed, try using it in the Task create by createParseTask() instead!
+ */
+ [[nodiscard]] virtual Task* createUpdateTask();
+
+ /** This creates a new parse task to be executed by onUpdateSucceeded().
+ *
+ * This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed
+ * in the background, so it slowly updates the UI as tasks get done.
+ */
+ [[nodiscard]] virtual Task* createParseTask(Resource const&) { return nullptr; };
+
+ /** Standard implementation of the model update logic.
+ *
+ * It uses set operations to find differences between the current state and the updated state,
+ * to act only on those disparities.
+ *
+ * The implementation is at the end of this header.
+ */
+ template <typename T>
+ void applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, T>& new_resources);
+
+ protected slots:
+ void directoryChanged(QString);
+
+ /** Called when the update task is successful.
+ *
+ * This usually calls static_cast on the specific Task type returned by createUpdateTask,
+ * so care must be taken in such cases.
+ * TODO: Figure out a way to express this relationship better without templated classes (Q_OBJECT macro disallows that).
+ */
+ virtual void onUpdateSucceeded();
+ virtual void onUpdateFailed() {}
+
+ /** Called when the parse task with the given ticket is successful.
+ *
+ * This is just a simple reference implementation. You probably want to override it with your own logic in a subclass
+ * if the resource is complex and has more stuff to parse.
+ */
+ virtual void onParseSucceeded(int ticket, QString resource_id);
+ virtual void onParseFailed(int ticket, QString resource_id) {}
+
+ protected:
+ // Represents the relationship between a column's index (represented by the list index), and it's sorting key.
+ // As such, the order in with they appear is very important!
+ QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE };
+
+ bool m_can_interact = true;
+
+ QDir m_dir;
+ QFileSystemWatcher m_watcher;
+ bool m_is_watching = false;
+
+ Task::Ptr m_current_update_task = nullptr;
+ bool m_scheduled_update = false;
+
+ QList<Resource::Ptr> m_resources;
+
+ // Represents the relationship between a resource's internal ID and it's row position on the model.
+ QMap<QString, int> m_resources_index;
+
+ QMap<int, Task::Ptr> m_active_parse_tasks;
+ int m_next_resolution_ticket = 0;
+ QMutex m_ticket_mutex;
+};
+
+/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */
+#define RESOURCE_HELPERS(T) \
+ [[nodiscard]] T* operator[](size_t index) \
+ { \
+ return static_cast<T*>(m_resources[index].get()); \
+ } \
+ [[nodiscard]] T* at(size_t index) \
+ { \
+ return static_cast<T*>(m_resources[index].get()); \
+ } \
+ [[nodiscard]] const T* at(size_t index) const \
+ { \
+ return static_cast<const T*>(m_resources.at(index).get()); \
+ } \
+ [[nodiscard]] T* first() \
+ { \
+ return static_cast<T*>(m_resources.first().get()); \
+ } \
+ [[nodiscard]] T* last() \
+ { \
+ return static_cast<T*>(m_resources.last().get()); \
+ } \
+ [[nodiscard]] T* find(QString id) \
+ { \
+ auto iter = std::find_if(m_resources.constBegin(), m_resources.constEnd(), \
+ [&](Resource::Ptr const& r) { return r->internal_id() == id; }); \
+ if (iter == m_resources.constEnd()) \
+ return nullptr; \
+ return static_cast<T*>((*iter).get()); \
+ }
+
+/* Template definition to avoid some code duplication */
+template <typename T>
+void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, T>& new_resources)
+{
+ // see if the kept resources changed in some way
+ {
+ QSet<QString> kept_set = current_set;
+ kept_set.intersect(new_set);
+
+ for (auto const& kept : kept_set) {
+ auto row_it = m_resources_index.constFind(kept);
+ Q_ASSERT(row_it != m_resources_index.constEnd());
+ auto row = row_it.value();
+
+ auto& new_resource = new_resources[kept];
+ auto const& current_resource = m_resources[row];
+
+ if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) {
+ // no significant change, ignore...
+ continue;
+ }
+
+ // If the resource is resolving, but something about it changed, we don't want to
+ // continue the resolving.
+ if (current_resource->isResolving()) {
+ auto task = (*m_active_parse_tasks.find(current_resource->resolutionTicket())).get();
+ task->abort();
+ }
+
+ m_resources[row].reset(new_resource);
+ resolveResource(m_resources.at(row));
+ emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
+ }
+ }
+
+ // remove resources no longer present
+ {
+ QSet<QString> removed_set = current_set;
+ removed_set.subtract(new_set);
+
+ QList<int> removed_rows;
+ for (auto& removed : removed_set)
+ removed_rows.append(m_resources_index[removed]);
+
+ std::sort(removed_rows.begin(), removed_rows.end(), std::greater<int>());
+
+ for (auto& removed_index : removed_rows) {
+ auto removed_it = m_resources.begin() + removed_index;
+
+ Q_ASSERT(removed_it != m_resources.end());
+ Q_ASSERT(removed_set.contains(removed_it->get()->internal_id()));
+
+ if ((*removed_it)->isResolving()) {
+ auto task = (*m_active_parse_tasks.find((*removed_it)->resolutionTicket())).get();
+ task->abort();
+ }
+
+ beginRemoveRows(QModelIndex(), removed_index, removed_index);
+ m_resources.erase(removed_it);
+ endRemoveRows();
+ }
+ }
+
+ // add new resources to the end
+ {
+ QSet<QString> added_set = new_set;
+ added_set.subtract(current_set);
+
+ // When you have a Qt build with assertions turned on, proceeding here will abort the application
+ if (added_set.size() > 0) {
+ beginInsertRows(QModelIndex(), m_resources.size(), m_resources.size() + added_set.size() - 1);
+
+ for (auto& added : added_set) {
+ auto res = new_resources[added];
+ m_resources.append(res);
+ resolveResource(res);
+ }
+
+ endInsertRows();
+ }
+ }
+
+ // update index
+ {
+ m_resources_index.clear();
+ int idx = 0;
+ for (auto const& mod : m_resources) {
+ m_resources_index[mod->internal_id()] = idx;
+ idx++;
+ }
+ }
+}
diff --git a/launcher/minecraft/mod/ResourceFolderModel_test.cpp b/launcher/minecraft/mod/ResourceFolderModel_test.cpp
new file mode 100644
index 00000000..fe98552e
--- /dev/null
+++ b/launcher/minecraft/mod/ResourceFolderModel_test.cpp
@@ -0,0 +1,275 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+* PolyMC - Minecraft Launcher
+* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+*
+* This program is free software: you can 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 <https://www.gnu.org/licenses/>.
+*
+* 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 <QTest>
+#include <QTemporaryDir>
+#include <QTimer>
+
+#include "FileSystem.h"
+
+#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/mod/ResourceFolderModel.h"
+
+#define EXEC_UPDATE_TASK(EXEC, VERIFY) \
+ QEventLoop loop; \
+ \
+ connect(&model, &ResourceFolderModel::updateFinished, &loop, &QEventLoop::quit); \
+ \
+ QTimer expire_timer; \
+ expire_timer.callOnTimeout(&loop, &QEventLoop::quit); \
+ expire_timer.setSingleShot(true); \
+ expire_timer.start(4000); \
+ \
+ VERIFY(EXEC); \
+ loop.exec(); \
+ \
+ QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); \
+ expire_timer.stop(); \
+ \
+ disconnect(&model, nullptr, nullptr, nullptr);
+
+class ResourceFolderModelTest : public QObject
+{
+ Q_OBJECT
+
+private
+slots:
+ // test for GH-1178 - install a folder with files to a mod list
+ void test_1178()
+ {
+ // source
+ QString source = QFINDTESTDATA("testdata/test_folder");
+
+ // sanity check
+ QVERIFY(!source.endsWith('/'));
+
+ auto verify = [](QString path)
+ {
+ QDir target_dir(FS::PathCombine(path, "test_folder"));
+ QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
+ QVERIFY(target_dir.entryList().contains("assets"));
+ };
+
+ // 1. test with no trailing /
+ {
+ QString folder = source;
+ QTemporaryDir tempDir;
+
+ QEventLoop loop;
+
+ ModFolderModel m(tempDir.path(), true);
+
+ connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
+
+ QTimer expire_timer;
+ expire_timer.callOnTimeout(&loop, &QEventLoop::quit);
+ expire_timer.setSingleShot(true);
+ expire_timer.start(4000);
+
+ m.installMod(folder);
+
+ loop.exec();
+
+ QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished.");
+ expire_timer.stop();
+
+ verify(tempDir.path());
+ }
+
+ // 2. test with trailing /
+ {
+ QString folder = source + '/';
+ QTemporaryDir tempDir;
+ QEventLoop loop;
+ ModFolderModel m(tempDir.path(), true);
+
+ connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
+
+ QTimer expire_timer;
+ expire_timer.callOnTimeout(&loop, &QEventLoop::quit);
+ expire_timer.setSingleShot(true);
+ expire_timer.start(4000);
+
+ m.installMod(folder);
+
+ loop.exec();
+
+ QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished.");
+ expire_timer.stop();
+
+ verify(tempDir.path());
+ }
+ }
+
+ void test_addFromWatch()
+ {
+ QString source = QFINDTESTDATA("testdata");
+
+ ModFolderModel model(source);
+
+ QCOMPARE(model.size(), 0);
+
+ EXEC_UPDATE_TASK(model.startWatching(), )
+
+ for (auto mod : model.allMods())
+ qDebug() << mod->name();
+
+ QCOMPARE(model.size(), 2);
+
+ model.stopWatching();
+
+ while (model.hasPendingParseTasks()) {
+ QTest::qSleep(20);
+ QCoreApplication::processEvents();
+ }
+ }
+
+ void test_removeResource()
+ {
+ QString folder_resource = QFINDTESTDATA("testdata/test_folder");
+ QString file_mod = QFINDTESTDATA("testdata/supercoolmod.jar");
+
+ QTemporaryDir tmp;
+
+ ResourceFolderModel model(QDir(tmp.path()));
+
+ QCOMPARE(model.size(), 0);
+
+ {
+ EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY)
+ }
+
+ QCOMPARE(model.size(), 1);
+ qDebug() << "Added first mod.";
+
+ {
+ EXEC_UPDATE_TASK(model.startWatching(), )
+ }
+
+ QCOMPARE(model.size(), 1);
+ qDebug() << "Started watching the temp folder.";
+
+ {
+ EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY)
+ }
+
+ QCOMPARE(model.size(), 2);
+ qDebug() << "Added second mod.";
+
+ {
+ EXEC_UPDATE_TASK(model.uninstallResource("supercoolmod.jar"), QVERIFY);
+ }
+
+ QCOMPARE(model.size(), 1);
+ qDebug() << "Removed first mod.";
+
+ QString mod_file_name {model.at(0).fileinfo().fileName()};
+ QVERIFY(!mod_file_name.isEmpty());
+
+ {
+ EXEC_UPDATE_TASK(model.uninstallResource(mod_file_name), QVERIFY);
+ }
+
+ QCOMPARE(model.size(), 0);
+ qDebug() << "Removed second mod.";
+
+ model.stopWatching();
+
+ while (model.hasPendingParseTasks()) {
+ QTest::qSleep(20);
+ QCoreApplication::processEvents();
+ }
+ }
+
+ void test_enable_disable()
+ {
+ QString folder_resource = QFINDTESTDATA("testdata/test_folder");
+ QString file_mod = QFINDTESTDATA("testdata/supercoolmod.jar");
+
+ QTemporaryDir tmp;
+ ResourceFolderModel model(tmp.path());
+
+ QCOMPARE(model.size(), 0);
+
+ {
+ EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY)
+ }
+ {
+ EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY)
+ }
+
+ for (auto res : model.all())
+ qDebug() << res->name();
+
+ QCOMPARE(model.size(), 2);
+
+ auto& res_1 = model.at(0).type() != ResourceType::FOLDER ? model.at(0) : model.at(1);
+ auto& res_2 = model.at(0).type() == ResourceType::FOLDER ? model.at(0) : model.at(1);
+ auto id_1 = res_1.internal_id();
+ auto id_2 = res_2.internal_id();
+ bool initial_enabled_res_2 = res_2.enabled();
+ bool initial_enabled_res_1 = res_1.enabled();
+
+ QVERIFY(res_1.type() != ResourceType::FOLDER && res_1.type() != ResourceType::UNKNOWN);
+ qDebug() << "res_1 is of the correct type.";
+ QVERIFY(res_1.enabled());
+ qDebug() << "res_1 is initially enabled.";
+
+ QVERIFY(res_1.enable(EnableAction::TOGGLE));
+
+ QVERIFY(res_1.enabled() == !initial_enabled_res_1);
+ qDebug() << "res_1 got successfully toggled.";
+
+ QVERIFY(res_1.enable(EnableAction::TOGGLE));
+ qDebug() << "res_1 got successfully toggled again.";
+
+ QVERIFY(res_1.enabled() == initial_enabled_res_1);
+ QVERIFY(res_1.internal_id() == id_1);
+ qDebug() << "res_1 got back to its initial state.";
+
+ QVERIFY(!res_2.enable(initial_enabled_res_2 ? EnableAction::ENABLE : EnableAction::DISABLE));
+ QVERIFY(res_2.enabled() == initial_enabled_res_2);
+ QVERIFY(res_2.internal_id() == id_2);
+
+ while (model.hasPendingParseTasks()) {
+ QTest::qSleep(20);
+ QCoreApplication::processEvents();
+ }
+ }
+};
+
+QTEST_GUILESS_MAIN(ResourceFolderModelTest)
+
+#include "ResourceFolderModel_test.moc"
diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h
new file mode 100644
index 00000000..c2cc8690
--- /dev/null
+++ b/launcher/minecraft/mod/ResourcePack.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "Resource.h"
+
+class ResourcePack : public Resource {
+ Q_OBJECT
+ public:
+ using Ptr = shared_qobject_ptr<Resource>;
+
+ ResourcePack(QObject* parent = nullptr) : Resource(parent) {}
+ ResourcePack(QFileInfo file_info) : Resource(file_info) {}
+
+};
diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp
index 276804ed..e92be894 100644
--- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp
+++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp
@@ -35,24 +35,4 @@
#include "ResourcePackFolderModel.h"
-ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) {
-}
-
-QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
- if (role == Qt::ToolTipRole) {
- switch (section) {
- case ActiveColumn:
- return tr("Is the resource pack enabled?");
- case NameColumn:
- return tr("The name of the resource pack.");
- case VersionColumn:
- return tr("The version of the resource pack.");
- case DateColumn:
- return tr("The date and time this resource pack was last changed (or added).");
- default:
- return QVariant();
- }
- }
-
- return ModFolderModel::headerData(section, orientation, role);
-}
+ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}
diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h
index 0cd6214b..1fe82867 100644
--- a/launcher/minecraft/mod/ResourcePackFolderModel.h
+++ b/launcher/minecraft/mod/ResourcePackFolderModel.h
@@ -1,13 +1,14 @@
#pragma once
-#include "ModFolderModel.h"
+#include "ResourceFolderModel.h"
-class ResourcePackFolderModel : public ModFolderModel
+#include "ResourcePack.h"
+
+class ResourcePackFolderModel : public ResourceFolderModel
{
Q_OBJECT
-
public:
explicit ResourcePackFolderModel(const QString &dir);
- QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+ RESOURCE_HELPERS(ResourcePack)
};
diff --git a/launcher/minecraft/mod/ShaderPackFolderModel.h b/launcher/minecraft/mod/ShaderPackFolderModel.h
new file mode 100644
index 00000000..a3aa958f
--- /dev/null
+++ b/launcher/minecraft/mod/ShaderPackFolderModel.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "ResourceFolderModel.h"
+
+class ShaderPackFolderModel : public ResourceFolderModel {
+ Q_OBJECT
+
+ public:
+ explicit ShaderPackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir)) {}
+};
diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp
index e3a22219..2c7c945b 100644
--- a/launcher/minecraft/mod/TexturePackFolderModel.cpp
+++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp
@@ -35,24 +35,4 @@
#include "TexturePackFolderModel.h"
-TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) {
-}
-
-QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
- if (role == Qt::ToolTipRole) {
- switch (section) {
- case ActiveColumn:
- return tr("Is the texture pack enabled?");
- case NameColumn:
- return tr("The name of the texture pack.");
- case VersionColumn:
- return tr("The version of the texture pack.");
- case DateColumn:
- return tr("The date and time this texture pack was last changed (or added).");
- default:
- return QVariant();
- }
- }
-
- return ModFolderModel::headerData(section, orientation, role);
-}
+TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}
diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h
index a59d5119..69e98661 100644
--- a/launcher/minecraft/mod/TexturePackFolderModel.h
+++ b/launcher/minecraft/mod/TexturePackFolderModel.h
@@ -1,13 +1,11 @@
#pragma once
-#include "ModFolderModel.h"
+#include "ResourceFolderModel.h"
-class TexturePackFolderModel : public ModFolderModel
+class TexturePackFolderModel : public ResourceFolderModel
{
Q_OBJECT
public:
explicit TexturePackFolderModel(const QString &dir);
-
- QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
};
diff --git a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h
new file mode 100644
index 00000000..cc02a9b9
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include <QDir>
+#include <QMap>
+#include <QObject>
+
+#include <memory>
+
+#include "minecraft/mod/Resource.h"
+
+#include "tasks/Task.h"
+
+/** Very simple task that just loads a folder's contents directly.
+ */
+class BasicFolderLoadTask : public Task
+{
+ Q_OBJECT
+public:
+ struct Result {
+ QMap<QString, Resource::Ptr> resources;
+ };
+ using ResultPtr = std::shared_ptr<Result>;
+
+ [[nodiscard]] ResultPtr result() const {
+ return m_result;
+ }
+
+public:
+ BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result) {}
+
+ [[nodiscard]] bool canAbort() const override { return true; }
+ bool abort() override { m_aborted = true; return true; }
+
+ void executeTask() override
+ {
+ m_dir.refresh();
+ for (auto entry : m_dir.entryInfoList()) {
+ auto resource = new Resource(entry);
+ m_result->resources.insert(resource->internal_id(), resource);
+ }
+
+ if (m_aborted)
+ emitAborted();
+ else
+ emitSucceeded();
+ }
+
+private:
+ QDir m_dir;
+ ResultPtr m_result;
+
+ bool m_aborted = false;
+};
diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
index 1519f49d..c486bd46 100644
--- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
+++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
@@ -20,22 +20,22 @@ namespace {
// OLD format:
// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc
-std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
+ModDetails ReadMCModInfo(QByteArray contents)
{
- auto getInfoFromArray = [&](QJsonArray arr)->std::shared_ptr<ModDetails>
+ auto getInfoFromArray = [&](QJsonArray arr) -> ModDetails
{
if (!arr.at(0).isObject()) {
- return nullptr;
+ return {};
}
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
auto firstObj = arr.at(0).toObject();
- details->mod_id = firstObj.value("modid").toString();
+ details.mod_id = firstObj.value("modid").toString();
auto name = firstObj.value("name").toString();
// NOTE: ignore stupid example mods copies where the author didn't even bother to change the name
if(name != "Example Mod") {
- details->name = name;
+ details.name = name;
}
- details->version = firstObj.value("version").toString();
+ details.version = firstObj.value("version").toString();
auto homeurl = firstObj.value("url").toString().trimmed();
if(!homeurl.isEmpty())
{
@@ -45,8 +45,8 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
homeurl.prepend("http://");
}
}
- details->homeurl = homeurl;
- details->description = firstObj.value("description").toString();
+ details.homeurl = homeurl;
+ details.description = firstObj.value("description").toString();
QJsonArray authors = firstObj.value("authorList").toArray();
if (authors.size() == 0) {
// FIXME: what is the format of this? is there any?
@@ -55,7 +55,7 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
for (auto author: authors)
{
- details->authors.append(author.toString());
+ details.authors.append(author.toString());
}
return details;
};
@@ -83,7 +83,7 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
{
qCritical() << "BAD stuff happened to mod json:";
qCritical() << contents;
- return nullptr;
+ return {};
}
auto arrVal = jsonDoc.object().value("modlist");
if(arrVal.isUndefined()) {
@@ -94,13 +94,13 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
return getInfoFromArray(arrVal.toArray());
}
}
- return nullptr;
+ return {};
}
// https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md
-std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
+ModDetails ReadMCModTOML(QByteArray contents)
{
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
char errbuf[200];
// top-level table
@@ -108,7 +108,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
if(!tomlData)
{
- return nullptr;
+ return {};
}
// array defined by [[mods]]
@@ -116,7 +116,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
if(!tomlModsArr)
{
qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!";
- return nullptr;
+ return {};
}
// we only really care about the first element, since multiple mods in one file is not supported by us at the moment
@@ -124,33 +124,33 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
if(!tomlModsTable0)
{
qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!";
- return nullptr;
+ return {};
}
// mandatory properties - always in [[mods]]
toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId");
if(modIdDatum.ok)
{
- details->mod_id = modIdDatum.u.s;
+ details.mod_id = modIdDatum.u.s;
// library says this is required for strings
free(modIdDatum.u.s);
}
toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version");
if(versionDatum.ok)
{
- details->version = versionDatum.u.s;
+ details.version = versionDatum.u.s;
free(versionDatum.u.s);
}
toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName");
if(displayNameDatum.ok)
{
- details->name = displayNameDatum.u.s;
+ details.name = displayNameDatum.u.s;
free(displayNameDatum.u.s);
}
toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description");
if(descriptionDatum.ok)
{
- details->description = descriptionDatum.u.s;
+ details.description = descriptionDatum.u.s;
free(descriptionDatum.u.s);
}
@@ -173,7 +173,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
}
if(!authors.isEmpty())
{
- details->authors.append(authors);
+ details.authors.append(authors);
}
toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL");
@@ -200,7 +200,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
homeurl.prepend("http://");
}
}
- details->homeurl = homeurl;
+ details.homeurl = homeurl;
// this seems to be recursive, so it should free everything
toml_free(tomlData);
@@ -209,20 +209,20 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
}
// https://fabricmc.net/wiki/documentation:fabric_mod_json
-std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
+ModDetails ReadFabricModInfo(QByteArray contents)
{
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object();
auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0;
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
- details->mod_id = object.value("id").toString();
- details->version = object.value("version").toString();
+ details.mod_id = object.value("id").toString();
+ details.version = object.value("version").toString();
- details->name = object.contains("name") ? object.value("name").toString() : details->mod_id;
- details->description = object.value("description").toString();
+ details.name = object.contains("name") ? object.value("name").toString() : details.mod_id;
+ details.description = object.value("description").toString();
if (schemaVersion >= 1)
{
@@ -230,10 +230,10 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
for (auto author: authors)
{
if(author.isObject()) {
- details->authors.append(author.toObject().value("name").toString());
+ details.authors.append(author.toObject().value("name").toString());
}
else {
- details->authors.append(author.toString());
+ details.authors.append(author.toString());
}
}
@@ -243,7 +243,7 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
if (contact.contains("homepage"))
{
- details->homeurl = contact.value("homepage").toString();
+ details.homeurl = contact.value("homepage").toString();
}
}
}
@@ -251,50 +251,50 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
}
// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md
-std::shared_ptr<ModDetails> ReadQuiltModInfo(QByteArray contents)
+ModDetails ReadQuiltModInfo(QByteArray contents)
{
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = Json::requireObject(jsonDoc, "quilt.mod.json");
auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version");
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
// 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");
- details->mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
- details->version = Json::requireString(modInfo.value("version"), "Mod version");
+ 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"));
+ 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();
+ details.authors += modContributors.keys();
auto modContact = Json::ensureObject(modMetadata.value("contact"));
if (modContact.contains("homepage"))
{
- details->homeurl = Json::requireString(modContact.value("homepage"));
+ details.homeurl = Json::requireString(modContact.value("homepage"));
}
}
return details;
}
-std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
+ModDetails ReadForgeInfo(QByteArray contents)
{
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
// Read the data
- details->name = "Minecraft Forge";
- details->mod_id = "Forge";
- details->homeurl = "http://www.minecraftforge.net/forum/";
+ details.name = "Minecraft Forge";
+ details.mod_id = "Forge";
+ details.homeurl = "http://www.minecraftforge.net/forum/";
INIFile ini;
if (!ini.loadFile(contents))
return details;
@@ -304,47 +304,47 @@ std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
QString revision = ini.get("forge.revision.number", "0").toString();
QString build = ini.get("forge.build.number", "0").toString();
- details->version = major + "." + minor + "." + revision + "." + build;
+ details.version = major + "." + minor + "." + revision + "." + build;
return details;
}
-std::shared_ptr<ModDetails> ReadLiteModInfo(QByteArray contents)
+ModDetails ReadLiteModInfo(QByteArray contents)
{
- std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ ModDetails details;
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object();
if (object.contains("name"))
{
- details->mod_id = details->name = object.value("name").toString();
+ details.mod_id = details.name = object.value("name").toString();
}
if (object.contains("version"))
{
- details->version = object.value("version").toString("");
+ details.version = object.value("version").toString("");
}
else
{
- details->version = object.value("revision").toString("");
+ details.version = object.value("revision").toString("");
}
- details->mcversion = object.value("mcversion").toString();
+ details.mcversion = object.value("mcversion").toString();
auto author = object.value("author").toString();
if(!author.isEmpty()) {
- details->authors.append(author);
+ details.authors.append(author);
}
- details->description = object.value("description").toString();
- details->homeurl = object.value("url").toString();
+ details.description = object.value("description").toString();
+ details.homeurl = object.value("url").toString();
return details;
}
}
-LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile):
+LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile):
+ Task(nullptr, false),
m_token(token),
m_type(type),
m_modFile(modFile),
m_result(new Result())
-{
-}
+{}
void LocalModParseTask::processAsZip()
{
@@ -366,7 +366,7 @@ void LocalModParseTask::processAsZip()
file.close();
// to replace ${file.jarVersion} with the actual version, as needed
- if (m_result->details && m_result->details->version == "${file.jarVersion}")
+ if (m_result->details.version == "${file.jarVersion}")
{
if (zip.setCurrentFile("META-INF/MANIFEST.MF"))
{
@@ -395,7 +395,7 @@ void LocalModParseTask::processAsZip()
manifestVersion = "NONE";
}
- m_result->details->version = manifestVersion;
+ m_result->details.version = manifestVersion;
file.close();
}
@@ -497,21 +497,31 @@ void LocalModParseTask::processAsLitemod()
zip.close();
}
-void LocalModParseTask::run()
+bool LocalModParseTask::abort()
+{
+ m_aborted = true;
+ return true;
+}
+
+void LocalModParseTask::executeTask()
{
switch(m_type)
{
- case Mod::MOD_ZIPFILE:
+ case ResourceType::ZIPFILE:
processAsZip();
break;
- case Mod::MOD_FOLDER:
+ case ResourceType::FOLDER:
processAsFolder();
break;
- case Mod::MOD_LITEMOD:
+ case ResourceType::LITEMOD:
processAsLitemod();
break;
default:
break;
}
- emit finished(m_token);
+
+ if (m_aborted)
+ emitAborted();
+ else
+ emitSucceeded();
}
diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h
index ed92394c..4bbf3c85 100644
--- a/launcher/minecraft/mod/tasks/LocalModParseTask.h
+++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h
@@ -2,29 +2,31 @@
#include <QDebug>
#include <QObject>
-#include <QRunnable>
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/ModDetails.h"
-class LocalModParseTask : public QObject, public QRunnable
+#include "tasks/Task.h"
+
+class LocalModParseTask : public Task
{
Q_OBJECT
public:
struct Result {
- QString id;
- std::shared_ptr<ModDetails> details;
+ ModDetails details;
};
using ResultPtr = std::shared_ptr<Result>;
ResultPtr result() const {
return m_result;
}
- LocalModParseTask(int token, Mod::ModType type, const QFileInfo & modFile);
- void run();
+ [[nodiscard]] bool canAbort() const override { return true; }
+ bool abort() override;
+
+ LocalModParseTask(int token, ResourceType type, const QFileInfo & modFile);
+ void executeTask() override;
-signals:
- void finished(int token);
+ [[nodiscard]] int token() const { return m_token; }
private:
void processAsZip();
@@ -33,7 +35,9 @@ private:
private:
int m_token;
- Mod::ModType m_type;
+ ResourceType m_type;
QFileInfo m_modFile;
ResultPtr m_result;
+
+ bool m_aborted = false;
};
diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
index a2e055ba..a56ba8ab 100644
--- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
+++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
@@ -38,11 +38,11 @@
#include "minecraft/mod/MetadataHandler.h"
-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())
+ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan, QObject* parent)
+ : Task(parent, false), m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result())
{}
-void ModFolderLoadTask::run()
+void ModFolderLoadTask::executeTask()
{
if (m_is_indexed) {
// Read metadata first
@@ -52,7 +52,7 @@ void ModFolderLoadTask::run()
// Read JAR files that don't have metadata
m_mods_dir.refresh();
for (auto entry : m_mods_dir.entryInfoList()) {
- Mod::Ptr mod(new Mod(entry));
+ Mod* mod(new Mod(entry));
if (mod->enabled()) {
if (m_result->mods.contains(mod->internal_id())) {
@@ -83,7 +83,20 @@ void ModFolderLoadTask::run()
}
}
- emit succeeded();
+ // Remove orphan metadata to prevent issues
+ // See https://github.com/PolyMC/PolyMC/issues/996
+ if (m_clean_orphan) {
+ QMutableMapIterator<QString, Mod::Ptr> iter(m_result->mods);
+ while (iter.hasNext()) {
+ auto mod = iter.next().value();
+ if (mod->status() == ModStatus::NotInstalled) {
+ mod->destroy(m_index_dir, false);
+ iter.remove();
+ }
+ }
+ }
+
+ emitSucceeded();
}
void ModFolderLoadTask::getFromMetadata()
diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h
index 0b6bb6cc..840e95e1 100644
--- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h
+++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h
@@ -42,8 +42,9 @@
#include <QRunnable>
#include <memory>
#include "minecraft/mod/Mod.h"
+#include "tasks/Task.h"
-class ModFolderLoadTask : public QObject, public QRunnable
+class ModFolderLoadTask : public Task
{
Q_OBJECT
public:
@@ -56,16 +57,16 @@ public:
}
public:
- ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed);
- void run();
-signals:
- void succeeded();
+ ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false, QObject* parent = nullptr);
+
+ void executeTask() override;
private:
void getFromMetadata();
private:
- QDir& m_mods_dir, m_index_dir;
+ QDir m_mods_dir, m_index_dir;
bool m_is_indexed;
+ bool m_clean_orphan;
ResultPtr m_result;
};
diff --git a/launcher/minecraft/mod/testdata/supercoolmod.jar b/launcher/minecraft/mod/testdata/supercoolmod.jar
new file mode 100644
index 00000000..d8cf9860
--- /dev/null
+++ b/launcher/minecraft/mod/testdata/supercoolmod.jar
@@ -0,0 +1 @@
+the best mod.
diff --git a/launcher/minecraft/update/FMLLibrariesTask.cpp b/launcher/minecraft/update/FMLLibrariesTask.cpp
index b6238ce9..7a0bd2f3 100644
--- a/launcher/minecraft/update/FMLLibrariesTask.cpp
+++ b/launcher/minecraft/update/FMLLibrariesTask.cpp
@@ -63,11 +63,12 @@ void FMLLibrariesTask::executeTask()
setStatus(tr("Downloading FML libraries..."));
auto dljob = new NetJob("FML libraries", APPLICATION->network());
auto metacache = APPLICATION->metacache();
+ Net::Download::Options options = Net::Download::Option::MakeEternal;
for (auto &lib : fmlLibsToProcess)
{
auto entry = metacache->resolveEntry("fmllibs", lib.filename);
QString urlString = BuildConfig.FMLLIBS_BASE_URL + lib.filename;
- dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry));
+ dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry, options));
}
connect(dljob, &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished);
diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp
index 60c54c4e..234330a7 100644
--- a/launcher/modplatform/EnsureMetadataTask.cpp
+++ b/launcher/modplatform/EnsureMetadataTask.cpp
@@ -3,81 +3,73 @@
#include <MurmurHash2.h>
#include <QDebug>
-#include "FileSystem.h"
#include "Json.h"
+
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
+
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
+
#include "net/NetJob.h"
-#include "tasks/MultipleOptionsTask.h"
static ModPlatform::ProviderCapabilities ProviderCaps;
static ModrinthAPI modrinth_api;
static FlameAPI flame_api;
-EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov)
+EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov)
+ : Task(nullptr), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr)
{
- auto hash = getHash(mod);
- if (hash.isEmpty())
- emitFail(mod);
- else
- m_mods.insert(hash, mod);
+ auto hash_task = createNewHash(mod);
+ if (!hash_task)
+ return;
+ connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); });
+ connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); });
+ hash_task->start();
}
EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::Provider prov)
- : Task(nullptr), m_index_dir(dir), m_provider(prov)
+ : Task(nullptr), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
{
+ m_hashing_task = new ConcurrentTask(this, "MakeHashesTask", 10);
for (auto* mod : mods) {
- if (!mod->valid()) {
- emitFail(mod);
- continue;
- }
-
- auto hash = getHash(mod);
- if (hash.isEmpty()) {
- emitFail(mod);
+ auto hash_task = createNewHash(mod);
+ if (!hash_task)
continue;
- }
-
- m_mods.insert(hash, mod);
+ connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); });
+ connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); });
+ m_hashing_task->addTask(hash_task);
}
}
-QString EnsureMetadataTask::getHash(Mod* mod)
+Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Mod* mod)
{
- /* Here we create a mapping hash -> mod, because we need that relationship to parse the API routes */
- QByteArray jar_data;
- try {
- jar_data = FS::read(mod->fileinfo().absoluteFilePath());
- } catch (FS::FileSystemException& e) {
- qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name());
- qCritical() << QString("Reason: ") << e.cause();
+ if (!mod || !mod->valid() || mod->type() == ResourceType::FOLDER)
+ return nullptr;
- return {};
- }
-
- switch (m_provider) {
- case ModPlatform::Provider::MODRINTH: {
- auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+ return Hashing::createHasher(mod->fileinfo().absoluteFilePath(), m_provider);
+}
- return QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, hash_type).toHex());
- }
- case ModPlatform::Provider::FLAME: {
- QByteArray jar_data_treated;
- for (char c : jar_data) {
- // CF-specific
- if (!(c == 9 || c == 10 || c == 13 || c == 32))
- jar_data_treated.push_back(c);
- }
+QString EnsureMetadataTask::getExistingHash(Mod* mod)
+{
+ // Check for already computed hashes
+ // (linear on the number of mods vs. linear on the size of the mod's JAR)
+ auto it = m_mods.keyValueBegin();
+ while (it != m_mods.keyValueEnd()) {
+ if ((*it).second == mod)
+ break;
+ it++;
+ }
- return QString::number(MurmurHash2(jar_data_treated, jar_data_treated.length()));
- }
+ // We already have the hash computed
+ if (it != m_mods.keyValueEnd()) {
+ return (*it).first;
}
+ // No existing hash
return {};
}
@@ -110,7 +102,7 @@ void EnsureMetadataTask::executeTask()
}
// Folders don't have metadata
- if (mod->type() == Mod::MOD_FOLDER) {
+ if (mod->type() == ResourceType::FOLDER) {
emitReady(mod);
}
}
@@ -127,11 +119,9 @@ void EnsureMetadataTask::executeTask()
}
auto invalidade_leftover = [this] {
- QMutableHashIterator<QString, Mod*> mods_iter(m_mods);
- while (mods_iter.hasNext()) {
- auto mod = mods_iter.next();
- emitFail(mod.value());
- }
+ for (auto mod = m_mods.constBegin(); mod != m_mods.constEnd(); mod++)
+ emitFail(mod.value(), mod.key(), RemoveFromList::No);
+ m_mods.clear();
emitSucceeded();
};
@@ -178,20 +168,44 @@ void EnsureMetadataTask::executeTask()
version_task->start();
}
-void EnsureMetadataTask::emitReady(Mod* m)
+void EnsureMetadataTask::emitReady(Mod* m, QString key, RemoveFromList remove)
{
+ if (!m) {
+ qCritical() << "Tried to mark a null mod as ready.";
+ if (!key.isEmpty())
+ m_mods.remove(key);
+
+ return;
+ }
+
qDebug() << QString("Generated metadata for %1").arg(m->name());
emit metadataReady(m);
- m_mods.remove(getHash(m));
+ if (remove == RemoveFromList::Yes) {
+ if (key.isEmpty())
+ key = getExistingHash(m);
+ m_mods.remove(key);
+ }
}
-void EnsureMetadataTask::emitFail(Mod* m)
+void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove)
{
+ if (!m) {
+ qCritical() << "Tried to mark a null mod as failed.";
+ if (!key.isEmpty())
+ m_mods.remove(key);
+
+ return;
+ }
+
qDebug() << QString("Failed to generate metadata for %1").arg(m->name());
emit metadataFailed(m);
- m_mods.remove(getHash(m));
+ if (remove == RemoveFromList::Yes) {
+ if (key.isEmpty())
+ key = getExistingHash(m);
+ m_mods.remove(key);
+ }
}
// Modrinth
diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h
index 79db6976..a8b0851e 100644
--- a/launcher/modplatform/EnsureMetadataTask.h
+++ b/launcher/modplatform/EnsureMetadataTask.h
@@ -1,12 +1,14 @@
#pragma once
#include "ModIndex.h"
-#include "tasks/SequentialTask.h"
#include "net/NetJob.h"
+#include "modplatform/helpers/HashUtils.h"
+
+#include "tasks/ConcurrentTask.h"
+
class Mod;
class QDir;
-class MultipleOptionsTask;
class EnsureMetadataTask : public Task {
Q_OBJECT
@@ -17,6 +19,8 @@ class EnsureMetadataTask : public Task {
~EnsureMetadataTask() = default;
+ Task::Ptr getHashingTask() { return m_hashing_task; }
+
public slots:
bool abort() override;
protected slots:
@@ -31,10 +35,16 @@ class EnsureMetadataTask : public Task {
auto flameProjectsTask() -> NetJob::Ptr;
// Helpers
- void emitReady(Mod*);
- void emitFail(Mod*);
+ enum class RemoveFromList {
+ Yes,
+ No
+ };
+ void emitReady(Mod*, QString key = {}, RemoveFromList = RemoveFromList::Yes);
+ void emitFail(Mod*, QString key = {}, RemoveFromList = RemoveFromList::Yes);
- auto getHash(Mod*) -> QString;
+ // Hashes and stuff
+ auto createNewHash(Mod*) -> Hashing::Hasher::Ptr;
+ auto getExistingHash(Mod*) -> QString;
private slots:
void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*);
@@ -50,5 +60,6 @@ class EnsureMetadataTask : public Task {
ModPlatform::Provider m_provider;
QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
+ ConcurrentTask* m_hashing_task;
NetJob* m_current_task;
};
diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp
index 3c4b7887..34fd9f30 100644
--- a/launcher/modplatform/ModIndex.cpp
+++ b/launcher/modplatform/ModIndex.cpp
@@ -19,6 +19,8 @@
#include "modplatform/ModIndex.h"
#include <QCryptographicHash>
+#include <QDebug>
+#include <QIODevice>
namespace ModPlatform {
@@ -53,34 +55,26 @@ auto ProviderCapabilities::hashType(Provider p) -> QStringList
}
return {};
}
-auto ProviderCapabilities::hash(Provider p, QByteArray& data, QString type) -> QByteArray
+
+auto ProviderCapabilities::hash(Provider p, QIODevice* device, QString type) -> QString
{
+ QCryptographicHash::Algorithm algo = QCryptographicHash::Sha1;
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);
+ algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Sha512;
+ break;
}
case Provider::FLAME:
- // 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);
- }
-
+ algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Md5;
break;
}
- return {};
+
+ QCryptographicHash hash(algo);
+ if(!hash.addData(device))
+ qCritical() << "Failed to read JAR to create hash!";
+
+ Q_ASSERT(hash.result().length() == hash.hashLength(algo));
+ return { hash.result().toHex() };
}
} // namespace ModPlatform
diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h
index bd3c28e3..518fed7c 100644
--- a/launcher/modplatform/ModIndex.h
+++ b/launcher/modplatform/ModIndex.h
@@ -24,6 +24,8 @@
#include <QVariant>
#include <QVector>
+class QIODevice;
+
namespace ModPlatform {
enum class Provider {
@@ -36,7 +38,7 @@ class ProviderCapabilities {
auto name(Provider) -> const char*;
auto readableName(Provider) -> QString;
auto hashType(Provider) -> QStringList;
- auto hash(Provider, QByteArray&, QString type = "") -> QByteArray;
+ auto hash(Provider, QIODevice*, QString type = "") -> QString;
};
struct ModpackAuthor {
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
index 0ed0ad29..70a35395 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -60,12 +60,13 @@ namespace ATLauncher {
static Meta::VersionPtr getComponentVersion(const QString& uid, const QString& version);
-PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString packName, QString version)
+PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString packName, QString version, InstallMode installMode)
{
m_support = support;
m_pack_name = packName;
m_pack_safe_name = packName.replace(QRegularExpression("[^A-Za-z0-9]"), "");
m_version_name = version;
+ m_install_mode = installMode;
}
bool PackInstallTask::abort()
@@ -117,9 +118,30 @@ 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);
+ // Derived from the installation mode
+ QString message;
+ bool resetDirectory;
+
+ switch (m_install_mode) {
+ case InstallMode::Reinstall:
+ case InstallMode::Update:
+ message = m_version.messages.update;
+ resetDirectory = true;
+ break;
+
+ case InstallMode::Install:
+ message = m_version.messages.install;
+ resetDirectory = false;
+ break;
+
+ default:
+ emitFailed(tr("Unsupported installation mode"));
+ return;
+ }
+
+ // Display message if one exists
+ if (!message.isEmpty())
+ m_support->displayMessage(message);
auto ver = getComponentVersion("net.minecraft", m_version.minecraft);
if (!ver) {
@@ -128,6 +150,10 @@ void PackInstallTask::onDownloadSucceeded()
}
minecraftVersion = ver;
+ if (resetDirectory) {
+ deleteExistingFiles();
+ }
+
if(m_version.noConfigs) {
downloadMods();
}
@@ -143,6 +169,116 @@ void PackInstallTask::onDownloadFailed(QString reason)
emitFailed(reason);
}
+void PackInstallTask::deleteExistingFiles()
+{
+ setStatus(tr("Deleting existing files..."));
+
+ // Setup defaults, as per https://wiki.atlauncher.com/pack-admin/xml/delete
+ VersionDeletes deletes;
+ deletes.folders.append(VersionDelete{ "root", "mods%s%" });
+ deletes.folders.append(VersionDelete{ "root", "configs%s%" });
+ deletes.folders.append(VersionDelete{ "root", "bin%s%" });
+
+ // Setup defaults, as per https://wiki.atlauncher.com/pack-admin/xml/keep
+ VersionKeeps keeps;
+ keeps.files.append(VersionKeep{ "root", "mods%s%PortalGunSounds.pak" });
+ keeps.folders.append(VersionKeep{ "root", "mods%s%rei_minimap%s%" });
+ keeps.folders.append(VersionKeep{ "root", "mods%s%VoxelMods%s%" });
+ keeps.files.append(VersionKeep{ "root", "config%s%NEI.cfg" });
+ keeps.files.append(VersionKeep{ "root", "options.txt" });
+ keeps.files.append(VersionKeep{ "root", "servers.dat" });
+
+ // Merge with version deletes and keeps
+ for (const auto& item : m_version.deletes.files)
+ deletes.files.append(item);
+ for (const auto& item : m_version.deletes.folders)
+ deletes.folders.append(item);
+ for (const auto& item : m_version.keeps.files)
+ keeps.files.append(item);
+ for (const auto& item : m_version.keeps.folders)
+ keeps.folders.append(item);
+
+ auto getPathForBase = [this](const QString& base) {
+ auto minecraftPath = FS::PathCombine(m_stagingPath, "minecraft");
+
+ if (base == "root") {
+ return minecraftPath;
+ }
+ else if (base == "config") {
+ return FS::PathCombine(minecraftPath, "config");
+ }
+ else {
+ qWarning() << "Unrecognised base path" << base;
+ return minecraftPath;
+ }
+ };
+
+ auto convertToSystemPath = [](const QString& path) {
+ auto t = path;
+ t.replace("%s%", QDir::separator());
+ return t;
+ };
+
+ auto shouldKeep = [keeps, getPathForBase, convertToSystemPath](const QString& fullPath) {
+ for (const auto& item : keeps.files) {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto path = FS::PathCombine(basePath, targetPath);
+
+ if (fullPath == path) {
+ return true;
+ }
+ }
+
+ for (const auto& item : keeps.folders) {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto path = FS::PathCombine(basePath, targetPath);
+
+ if (fullPath.startsWith(path)) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ // Keep track of files to delete
+ QSet<QString> filesToDelete;
+
+ for (const auto& item : deletes.files) {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto fullPath = FS::PathCombine(basePath, targetPath);
+
+ if (shouldKeep(fullPath))
+ continue;
+
+ filesToDelete.insert(fullPath);
+ }
+
+ for (const auto& item : deletes.folders) {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto fullPath = FS::PathCombine(basePath, targetPath);
+
+ QDirIterator it(fullPath, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ auto path = it.next();
+
+ if (shouldKeep(path))
+ continue;
+
+ filesToDelete.insert(path);
+ }
+ }
+
+ // Delete the files
+ for (const auto& item : filesToDelete) {
+ QFile::remove(item);
+ }
+}
+
QString PackInstallTask::getDirForModType(ModType type, QString raw)
{
switch (type) {
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h
index 992ba9c5..a7124d59 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h
@@ -50,6 +50,12 @@
namespace ATLauncher {
+enum class InstallMode {
+ Install,
+ Reinstall,
+ Update,
+};
+
class UserInteractionSupport {
public:
@@ -75,7 +81,7 @@ class PackInstallTask : public InstanceTask
Q_OBJECT
public:
- explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version);
+ explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version, InstallMode installMode = InstallMode::Install);
virtual ~PackInstallTask(){}
bool canAbort() const override { return true; }
@@ -99,6 +105,7 @@ private:
bool createLibrariesComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile);
bool createPackComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile);
+ void deleteExistingFiles();
void installConfigs();
void extractConfigs();
void downloadMods();
@@ -117,6 +124,7 @@ private:
NetJob::Ptr jobPtr;
QByteArray response;
+ InstallMode m_install_mode;
QString m_pack_name;
QString m_pack_safe_name;
QString m_version_name;
diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp
index 3af02a09..5a458f4e 100644
--- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp
@@ -224,6 +224,64 @@ static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a,
a.depends = Json::ensureString(obj, "depends", "");
}
+static void loadVersionKeep(ATLauncher::VersionKeep& k, QJsonObject& obj)
+{
+ k.base = Json::requireString(obj, "base");
+ k.target = Json::requireString(obj, "target");
+}
+
+static void loadVersionKeeps(ATLauncher::VersionKeeps& k, QJsonObject& obj)
+{
+ if (obj.contains("files")) {
+ auto files = Json::requireArray(obj, "files");
+ for (const auto keepRaw : files) {
+ auto keepObj = Json::requireObject(keepRaw);
+ ATLauncher::VersionKeep keep;
+ loadVersionKeep(keep, keepObj);
+ k.files.append(keep);
+ }
+ }
+
+ if (obj.contains("folders")) {
+ auto folders = Json::requireArray(obj, "folders");
+ for (const auto keepRaw : folders) {
+ auto keepObj = Json::requireObject(keepRaw);
+ ATLauncher::VersionKeep keep;
+ loadVersionKeep(keep, keepObj);
+ k.folders.append(keep);
+ }
+ }
+}
+
+static void loadVersionDelete(ATLauncher::VersionDelete& d, QJsonObject& obj)
+{
+ d.base = Json::requireString(obj, "base");
+ d.target = Json::requireString(obj, "target");
+}
+
+static void loadVersionDeletes(ATLauncher::VersionDeletes& d, QJsonObject& obj)
+{
+ if (obj.contains("files")) {
+ auto files = Json::requireArray(obj, "files");
+ for (const auto deleteRaw : files) {
+ auto deleteObj = Json::requireObject(deleteRaw);
+ ATLauncher::VersionDelete versionDelete;
+ loadVersionDelete(versionDelete, deleteObj);
+ d.files.append(versionDelete);
+ }
+ }
+
+ if (obj.contains("folders")) {
+ auto folders = Json::requireArray(obj, "folders");
+ for (const auto deleteRaw : folders) {
+ auto deleteObj = Json::requireObject(deleteRaw);
+ ATLauncher::VersionDelete versionDelete;
+ loadVersionDelete(versionDelete, deleteObj);
+ d.folders.append(versionDelete);
+ }
+ }
+}
+
void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj)
{
v.version = Json::requireString(obj, "version");
@@ -284,4 +342,10 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj)
auto messages = Json::ensureObject(obj, "messages");
loadVersionMessages(v.messages, messages);
+
+ auto keeps = Json::ensureObject(obj, "keeps");
+ loadVersionKeeps(v.keeps, keeps);
+
+ auto deletes = Json::ensureObject(obj, "deletes");
+ loadVersionDeletes(v.deletes, deletes);
}
diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h
index 43510c50..571c976d 100644
--- a/launcher/modplatform/atlauncher/ATLPackManifest.h
+++ b/launcher/modplatform/atlauncher/ATLPackManifest.h
@@ -150,6 +150,26 @@ struct VersionMessages
QString update;
};
+struct VersionKeep {
+ QString base;
+ QString target;
+};
+
+struct VersionKeeps {
+ QVector<VersionKeep> files;
+ QVector<VersionKeep> folders;
+};
+
+struct VersionDelete {
+ QString base;
+ QString target;
+};
+
+struct VersionDeletes {
+ QVector<VersionDelete> files;
+ QVector<VersionDelete> folders;
+};
+
struct PackVersionMainClass
{
QString mainClass;
@@ -178,6 +198,9 @@ struct PackVersion
QMap<QString, QString> colours;
QMap<QString, QString> warnings;
VersionMessages messages;
+
+ VersionKeeps keeps;
+ VersionDeletes deletes;
};
void loadVersion(PackVersion & v, QJsonObject & obj);
diff --git a/launcher/modplatform/helpers/HashUtils.cpp b/launcher/modplatform/helpers/HashUtils.cpp
new file mode 100644
index 00000000..a7bbaba5
--- /dev/null
+++ b/launcher/modplatform/helpers/HashUtils.cpp
@@ -0,0 +1,81 @@
+#include "HashUtils.h"
+
+#include <QDebug>
+#include <QFile>
+
+#include "FileSystem.h"
+
+#include <MurmurHash2.h>
+
+namespace Hashing {
+
+static ModPlatform::ProviderCapabilities ProviderCaps;
+
+Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider)
+{
+ switch (provider) {
+ case ModPlatform::Provider::MODRINTH:
+ return createModrinthHasher(file_path);
+ case ModPlatform::Provider::FLAME:
+ return createFlameHasher(file_path);
+ default:
+ qCritical() << "[Hashing]"
+ << "Unrecognized mod platform!";
+ return nullptr;
+ }
+}
+
+Hasher::Ptr createModrinthHasher(QString file_path)
+{
+ return new ModrinthHasher(file_path);
+}
+
+Hasher::Ptr createFlameHasher(QString file_path)
+{
+ return new FlameHasher(file_path);
+}
+
+void ModrinthHasher::executeTask()
+{
+ QFile file(m_path);
+
+ try {
+ file.open(QFile::ReadOnly);
+ } catch (FS::FileSystemException& e) {
+ qCritical() << QString("Failed to open JAR file in %1").arg(m_path);
+ qCritical() << QString("Reason: ") << e.cause();
+
+ emitFailed("Failed to open file for hashing.");
+ return;
+ }
+
+ auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+ m_hash = ProviderCaps.hash(ModPlatform::Provider::MODRINTH, &file, hash_type);
+
+ file.close();
+
+ if (m_hash.isEmpty()) {
+ emitFailed("Empty hash!");
+ } else {
+ emitSucceeded();
+ }
+}
+
+void FlameHasher::executeTask()
+{
+ // CF-specific
+ auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); };
+
+ std::ifstream file_stream(m_path.toStdString(), std::ifstream::binary);
+ // TODO: This is very heavy work, but apparently QtConcurrent can't use move semantics, so we can't boop this to another thread.
+ // How do we make this non-blocking then?
+ m_hash = QString::number(MurmurHash2(std::move(file_stream), 4 * MiB, should_filter_out));
+
+ if (m_hash.isEmpty()) {
+ emitFailed("Empty hash!");
+ } else {
+ emitSucceeded();
+ }
+}
+
+} // namespace Hashing
diff --git a/launcher/modplatform/helpers/HashUtils.h b/launcher/modplatform/helpers/HashUtils.h
new file mode 100644
index 00000000..38fddf03
--- /dev/null
+++ b/launcher/modplatform/helpers/HashUtils.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include <QString>
+
+#include "modplatform/ModIndex.h"
+#include "tasks/Task.h"
+
+namespace Hashing {
+
+class Hasher : public Task {
+ public:
+ using Ptr = shared_qobject_ptr<Hasher>;
+
+ Hasher(QString file_path) : m_path(std::move(file_path)) {}
+
+ /* We can't really abort this task, but we can say we aborted and finish our thing quickly :) */
+ bool abort() override { return true; }
+
+ void executeTask() override = 0;
+
+ QString getResult() const { return m_hash; };
+ QString getPath() const { return m_path; };
+
+ protected:
+ QString m_hash;
+ QString m_path;
+};
+
+class FlameHasher : public Hasher {
+ public:
+ FlameHasher(QString file_path) : Hasher(file_path) { setObjectName(QString("FlameHasher: %1").arg(file_path)); }
+
+ void executeTask() override;
+};
+
+class ModrinthHasher : public Hasher {
+ public:
+ ModrinthHasher(QString file_path) : Hasher(file_path) { setObjectName(QString("ModrinthHasher: %1").arg(file_path)); }
+
+ void executeTask() override;
+};
+
+Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider);
+Hasher::Ptr createFlameHasher(QString file_path);
+Hasher::Ptr createModrinthHasher(QString file_path);
+
+} // namespace Hashing
diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
index 16013070..3c15667c 100644
--- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
+++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
@@ -48,7 +48,7 @@
#include "Application.h"
#include "BuildConfig.h"
-#include "ui/dialogs/ScrollMessageBox.h"
+#include "ui/dialogs/BlockedModsDialog.h"
namespace ModpacksCH {
@@ -173,6 +173,7 @@ void PackInstallTask::onResolveModsSucceeded()
m_abortable = false;
QString text;
+ QList<QUrl> urls;
auto anyBlocked = false;
Flame::Manifest results = m_mod_id_resolver_task->getResults();
@@ -190,6 +191,7 @@ void PackInstallTask::onResolveModsSucceeded()
type[0] = type[0].toUpper();
text += QString("%1: %2 - <a href='%3'>%3</a><br/>").arg(type, local_file.name, results_file.websiteUrl);
+ urls.append(QUrl(results_file.websiteUrl));
anyBlocked = true;
} else {
local_file.url = results_file.url.toString();
@@ -201,10 +203,11 @@ void PackInstallTask::onResolveModsSucceeded()
if (anyBlocked) {
qDebug() << "Blocked files found, displaying file list";
- auto message_dialog = new ScrollMessageBox(m_parent, tr("Blocked files found"),
+ auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked files found"),
tr("The following files are not available for download in third party launchers.<br/>"
"You will need to manually download them and add them to the instance."),
- text);
+ text,
+ urls);
if (message_dialog->exec() == QDialog::Accepted)
downloadPack();
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
index 79d8edf7..e2d27547 100644
--- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
@@ -2,11 +2,14 @@
#include "ModrinthAPI.h"
#include "ModrinthPackIndex.h"
-#include "FileSystem.h"
#include "Json.h"
#include "ModDownloadTask.h"
+#include "modplatform/helpers/HashUtils.h"
+
+#include "tasks/ConcurrentTask.h"
+
static ModrinthAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
@@ -32,6 +35,8 @@ void ModrinthCheckUpdate::executeTask()
// Create all hashes
QStringList hashes;
auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+
+ ConcurrentTask hashing_task(this, "MakeModrinthHashesTask", 10);
for (auto* mod : m_mods) {
if (!mod->enabled()) {
emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!"));
@@ -44,25 +49,25 @@ void ModrinthCheckUpdate::executeTask()
// need to generate a new hash if the current one is innadequate
// (though it will rarely happen, if at all)
if (mod->metadata()->hash_format != best_hash_type) {
- QByteArray jar_data;
-
- try {
- jar_data = FS::read(mod->fileinfo().absoluteFilePath());
- } catch (FS::FileSystemException& e) {
- qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name());
- qCritical() << QString("Reason: ") << e.cause();
-
- failed(e.what());
- return;
- }
-
- hash = QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, best_hash_type).toHex());
+ auto hash_task = Hashing::createModrinthHasher(mod->fileinfo().absoluteFilePath());
+ connect(hash_task.get(), &Task::succeeded, [&] {
+ QString hash (hash_task->getResult());
+ hashes.append(hash);
+ mappings.insert(hash, mod);
+ });
+ connect(hash_task.get(), &Task::failed, [this, hash_task] { failed("Failed to generate hash"); });
+ hashing_task.addTask(hash_task);
+ } else {
+ hashes.append(hash);
+ mappings.insert(hash, mod);
}
-
- hashes.append(hash);
- mappings.insert(hash, mod);
}
+ QEventLoop loop;
+ connect(&hashing_task, &Task::finished, [&loop]{ loop.quit(); });
+ hashing_task.start();
+ loop.exec();
+
auto* response = new QByteArray();
auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response);
diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp
index e6a6adcc..fd3dbedc 100644
--- a/launcher/net/Download.cpp
+++ b/launcher/net/Download.cpp
@@ -60,7 +60,7 @@ auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Down
dl->m_url = url;
dl->m_options = options;
auto md5Node = new ChecksumValidator(QCryptographicHash::Md5);
- auto cachedNode = new MetaCacheSink(entry, md5Node);
+ auto cachedNode = new MetaCacheSink(entry, md5Node, options.testFlag(Option::MakeEternal));
dl->m_sink.reset(cachedNode);
return dl;
}
@@ -118,7 +118,7 @@ void Download::executeTask()
}
request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8());
- if (APPLICATION->currentCapabilities() & Application::SupportsFlame
+ if (APPLICATION->capabilities() & Application::SupportsFlame
&& request.url().host().contains("api.curseforge.com")) {
request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8());
};
diff --git a/launcher/net/Download.h b/launcher/net/Download.h
index 1d264381..3faa5db5 100644
--- a/launcher/net/Download.h
+++ b/launcher/net/Download.h
@@ -49,7 +49,7 @@ class Download : public NetAction {
public:
using Ptr = shared_qobject_ptr<class Download>;
- enum class Option { NoOptions = 0, AcceptLocalFiles = 1 };
+ enum class Option { NoOptions = 0, AcceptLocalFiles = 1, MakeEternal = 2 };
Q_DECLARE_FLAGS(Options, Option)
protected:
diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp
index 4d86c0b8..9606ddb6 100644
--- a/launcher/net/HttpMetaCache.cpp
+++ b/launcher/net/HttpMetaCache.cpp
@@ -121,6 +121,14 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex
SaveEventually();
}
+ // Get rid of old entries, to prevent cache problems
+ auto current_time = QDateTime::currentSecsSinceEpoch();
+ if (entry->isExpired(current_time - ( file_last_changed / 1000 ))) {
+ qWarning() << "Removing cache entry because of old age!";
+ selected_base.entry_list.remove(resource_path);
+ return staleEntry(base, resource_path);
+ }
+
// entry passed all the checks we cared about.
entry->basePath = getBasePath(base);
return entry;
@@ -221,6 +229,13 @@ void HttpMetaCache::Load()
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");
+
+ foo->makeEternal(Json::ensureBoolean(element_obj, "eternal", false));
+ if (!foo->isEternal()) {
+ foo->current_age = Json::ensureDouble(element_obj, "current_age");
+ foo->max_age = Json::ensureDouble(element_obj, "max_age");
+ }
+
// presumed innocent until closer examination
foo->stale = false;
@@ -240,6 +255,8 @@ void HttpMetaCache::SaveNow()
if (m_index_file.isNull())
return;
+ qDebug() << "[HttpMetaCache]" << "Saving metacache with" << m_entries.size() << "entries";
+
QJsonObject toplevel;
Json::writeString(toplevel, "version", "1");
@@ -259,6 +276,12 @@ void HttpMetaCache::SaveNow()
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));
+ if (entry->isEternal()) {
+ entryObj.insert("eternal", true);
+ } else {
+ entryObj.insert("current_age", QJsonValue(double(entry->current_age)));
+ entryObj.insert("max_age", QJsonValue(double(entry->max_age)));
+ }
entriesArr.append(entryObj);
}
}
diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h
index e944b3d5..c0b12318 100644
--- a/launcher/net/HttpMetaCache.h
+++ b/launcher/net/HttpMetaCache.h
@@ -64,14 +64,31 @@ class MetaEntry {
auto getMD5Sum() -> QString { return md5sum; }
void setMD5Sum(QString md5sum) { this->md5sum = md5sum; }
+ /* Whether the entry expires after some time (false) or not (true). */
+ void makeEternal(bool eternal) { is_eternal = eternal; }
+ [[nodiscard]] bool isEternal() const { return is_eternal; }
+
+ auto getCurrentAge() -> qint64 { return current_age; }
+ void setCurrentAge(qint64 age) { current_age = age; }
+
+ auto getMaximumAge() -> qint64 { return max_age; }
+ void setMaximumAge(qint64 age) { max_age = age; }
+
+ bool isExpired(qint64 offset) { return !is_eternal && (current_age >= max_age - offset); };
+
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
+ qint64 current_age = 0;
+ qint64 max_age = 0;
+ bool is_eternal = false;
+
bool stale = true;
};
diff --git a/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp
index f86dd870..5ae53c1c 100644
--- a/launcher/net/MetaCacheSink.cpp
+++ b/launcher/net/MetaCacheSink.cpp
@@ -36,13 +36,18 @@
#include "MetaCacheSink.h"
#include <QFile>
#include <QFileInfo>
-#include "FileSystem.h"
#include "Application.h"
namespace Net {
-MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum)
- :Net::FileSink(entry->getFullPath()), m_entry(entry), m_md5Node(md5sum)
+/** Maximum time to hold a cache entry
+ * = 1 week in seconds
+ */
+#define MAX_TIME_TO_EXPIRE 1*7*24*60*60
+
+
+MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum, bool is_eternal)
+ :Net::FileSink(entry->getFullPath()), m_entry(entry), m_md5Node(md5sum), m_is_eternal(is_eternal)
{
addValidator(md5sum);
}
@@ -88,6 +93,40 @@ Task::State MetaCacheSink::finalizeCache(QNetworkReply & reply)
}
m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch());
+
+ { // Cache lifetime
+ if (m_is_eternal) {
+ qDebug() << "[MetaCache] Adding eternal cache entry:" << m_entry->getFullPath();
+ m_entry->makeEternal(true);
+ } else if (reply.hasRawHeader("Cache-Control")) {
+ auto cache_control_header = reply.rawHeader("Cache-Control");
+ // qDebug() << "[MetaCache] Parsing 'Cache-Control' header with" << cache_control_header;
+
+ QRegularExpression max_age_expr("max-age=([0-9]+)");
+ qint64 max_age = max_age_expr.match(cache_control_header).captured(1).toLongLong();
+ m_entry->setMaximumAge(max_age);
+
+ } else if (reply.hasRawHeader("Expires")) {
+ auto expires_header = reply.rawHeader("Expires");
+ // qDebug() << "[MetaCache] Parsing 'Expires' header with" << expires_header;
+
+ qint64 max_age = QDateTime::fromString(expires_header).toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch();
+ m_entry->setMaximumAge(max_age);
+ } else {
+ m_entry->setMaximumAge(MAX_TIME_TO_EXPIRE);
+ }
+
+ if (reply.hasRawHeader("Age")) {
+ auto age_header = reply.rawHeader("Age");
+ // qDebug() << "[MetaCache] Parsing 'Age' header with" << age_header;
+
+ qint64 current_age = age_header.toLongLong();
+ m_entry->setCurrentAge(current_age);
+ } else {
+ m_entry->setCurrentAge(0);
+ }
+ }
+
m_entry->setStale(false);
APPLICATION->metacache()->updateEntry(m_entry);
diff --git a/launcher/net/MetaCacheSink.h b/launcher/net/MetaCacheSink.h
index c9f7edfe..f5948085 100644
--- a/launcher/net/MetaCacheSink.h
+++ b/launcher/net/MetaCacheSink.h
@@ -42,7 +42,7 @@
namespace Net {
class MetaCacheSink : public FileSink {
public:
- MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum);
+ MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum, bool is_eternal = false);
virtual ~MetaCacheSink() = default;
auto hasLocalData() -> bool override;
@@ -54,5 +54,6 @@ class MetaCacheSink : public FileSink {
private:
MetaEntryPtr m_entry;
ChecksumValidator* m_md5Node;
+ bool m_is_eternal;
};
} // namespace Net
diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h
index 729d4132..d9c4fadc 100644
--- a/launcher/net/NetAction.h
+++ b/launcher/net/NetAction.h
@@ -54,6 +54,8 @@ class NetAction : public Task {
QUrl url() { return m_url; }
auto index() -> int { return m_index_within_job; }
+ void setNetwork(shared_qobject_ptr<QNetworkAccessManager> network) { m_network = network; }
+
protected slots:
virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0;
virtual void downloadError(QNetworkReply::NetworkError error) = 0;
diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp
index bab35fa5..20d75976 100644
--- a/launcher/net/NetJob.cpp
+++ b/launcher/net/NetJob.cpp
@@ -35,204 +35,90 @@
*/
#include "NetJob.h"
-#include "Download.h"
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, 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);
- }
+ action->m_index_within_job = m_queue.size();
+ m_queue.append(action);
+
+ action->setNetwork(m_network);
return true;
}
+void NetJob::startNext()
+{
+ if (m_queue.isEmpty() && m_doing.isEmpty()) {
+ // We're finished, check for failures and retry if we can (up to 3 times)
+ if (!m_failed.isEmpty() && m_try < 3) {
+ m_try += 1;
+ while (!m_failed.isEmpty())
+ m_queue.enqueue(m_failed.take(*m_failed.keyBegin()));
+ }
+ }
+
+ ConcurrentTask::startNext();
+}
+
+auto NetJob::size() const -> int
+{
+ return m_queue.size() + m_doing.size() + m_done.size();
+}
+
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];
+ for (auto part : m_queue)
canFullyAbort &= part->canAbort();
- }
+
// can abort the active downloads?
- for (auto index : m_doing) {
- auto part = m_downloads[index];
+ for (auto part : m_doing)
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
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- QSet<int> todoSet(m_todo.begin(), m_todo.end());
- m_failed.unite(todoSet);
-#else
- m_failed.unite(m_todo.toSet());
-#endif
- m_todo.clear();
+ for (auto task : m_queue)
+ m_failed.insert(task.get(), task);
+ m_queue.clear();
// abort active downloads
auto toKill = m_doing.values();
- for (auto index : toKill) {
- auto part = m_downloads[index];
+ for (auto part : toKill) {
fullyAborted &= part->abort();
}
return fullyAborted;
}
-void NetJob::partSucceeded(int index)
-{
- // do progress. all slots are 1 in size at least
- auto& slot = m_parts_progress[index];
- partProgress(index, slot.total_progress, slot.total_progress);
-
- m_doing.remove(index);
- m_done.insert(index);
- m_downloads[index].get()->disconnect(this);
-
- startMoreParts();
-}
-
-void NetJob::partFailed(int index)
+auto NetJob::getFailedActions() -> QList<NetAction*>
{
- m_doing.remove(index);
-
- auto& slot = m_parts_progress[index];
- // Can try 3 times before failing by definitive
- if (slot.failures == 3) {
- m_failed.insert(index);
- } else {
- slot.failures++;
- m_todo.enqueue(index);
+ QList<NetAction*> failed;
+ for (auto index : m_failed) {
+ failed.push_back(dynamic_cast<NetAction*>(index.get()));
}
-
- m_downloads[index].get()->disconnect(this);
-
- startMoreParts();
-}
-
-void NetJob::partAborted(int index)
-{
- m_aborted = true;
-
- m_doing.remove(index);
- m_failed.insert(index);
- m_downloads[index].get()->disconnect(this);
-
- startMoreParts();
+ return failed;
}
-void NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal)
+auto NetJob::getFailedFiles() -> QList<QString>
{
- 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 = m_parts_progress.size();
-
- qint64 bytesAll = 0;
- qint64 bytesTotalAll = 0;
- 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) {
- continue;
- }
- bytesAll += part.current_progress;
- bytesTotalAll += part.total_progress;
- }
-
- qint64 inprogress = (bytesTotalAll == 0) ? 0 : (bytesAll * 1000) / bytesTotalAll;
- auto current = done * 1000 + doing * inprogress;
- 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) {
- m_current_progress = inprogress;
- }
- if (m_current_progress > current) {
- current = m_current_progress;
+ QList<QString> failed;
+ for (auto index : m_failed) {
+ failed.append(static_cast<NetAction*>(index.get())->url().toString());
}
- m_current_progress = current;
- setProgress(current, current_total);
+ return failed;
}
-void NetJob::startMoreParts()
+void NetJob::updateState()
{
- 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()) {
- emitSucceeded();
- } else if (m_aborted) {
- emitAborted();
- } 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, 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 = m_downloads[doThis];
-
- // connect signals :D
- 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); });
- connect(part.get(), &NetAction::status, this, &NetJob::status);
-
- part->startAction(m_network);
- }
+ emit progress(m_done.count(), m_total_size);
+ setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)")
+ .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(m_total_size)));
}
diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h
index 63c1cf51..cd5d5e48 100644
--- a/launcher/net/NetJob.h
+++ b/launcher/net/NetJob.h
@@ -39,64 +39,40 @@
#include <QObject>
#include "NetAction.h"
-#include "tasks/Task.h"
+#include "tasks/ConcurrentTask.h"
// 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 ConcurrentTask {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<NetJob>;
- explicit NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> network) : Task(), m_network(network)
- {
- setObjectName(job_name);
- }
- virtual ~NetJob() = default;
+ explicit NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> network) : ConcurrentTask(nullptr, job_name), m_network(network) {}
+ ~NetJob() override = default;
- void executeTask() override;
+ void startNext() override;
- auto canAbort() const -> bool override;
+ auto size() const -> int;
+ auto canAbort() const -> bool override;
auto addNetAction(NetAction::Ptr action) -> bool;
- 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;
+ auto getFailedActions() -> QList<NetAction*>;
+ auto getFailedFiles() -> QList<QString>;
public slots:
// Qt can't handle auto at the start for some reason?
bool abort() override;
- private slots:
- void startMoreParts();
-
- void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal);
- void partSucceeded(int index);
- void partFailed(int index);
- void partAborted(int index);
+ protected:
+ void updateState() override;
private:
shared_qobject_ptr<QNetworkAccessManager> m_network;
- struct part_info {
- qint64 current_progress = 0;
- qint64 total_progress = 1;
- int failures = 0;
- };
-
- QList<NetAction::Ptr> m_downloads;
- QList<part_info> m_parts_progress;
- QQueue<int> m_todo;
- QSet<int> m_doing;
- QSet<int> m_done;
- QSet<int> m_failed;
- qint64 m_current_progress = 0;
- bool m_aborted = false;
+ int m_try = 1;
};
diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp
index cfda4b4e..f3b19022 100644
--- a/launcher/net/Upload.cpp
+++ b/launcher/net/Upload.cpp
@@ -216,7 +216,7 @@ namespace Net {
}
request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8());
- if (APPLICATION->currentCapabilities() & Application::SupportsFlame
+ if (APPLICATION->capabilities() & Application::SupportsFlame
&& request.url().host().contains("api.curseforge.com")) {
request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8());
}
diff --git a/launcher/settings/SettingsObject.h b/launcher/settings/SettingsObject.h
index 3d61e707..6200bc3a 100644
--- a/launcher/settings/SettingsObject.h
+++ b/launcher/settings/SettingsObject.h
@@ -25,6 +25,7 @@ class Setting;
class SettingsObject;
typedef std::shared_ptr<SettingsObject> SettingsObjectPtr;
+typedef std::weak_ptr<SettingsObject> SettingsObjectWeakPtr;
/*!
* \brief The SettingsObject handles communicating settings between the application and a
diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp
index b88cfb13..484ac58e 100644
--- a/launcher/tasks/ConcurrentTask.cpp
+++ b/launcher/tasks/ConcurrentTask.cpp
@@ -1,10 +1,11 @@
#include "ConcurrentTask.h"
#include <QDebug>
+#include <QCoreApplication>
ConcurrentTask::ConcurrentTask(QObject* parent, QString task_name, int max_concurrent)
: Task(parent), m_name(task_name), m_total_max_size(max_concurrent)
-{}
+{ setObjectName(task_name); }
ConcurrentTask::~ConcurrentTask()
{
@@ -36,31 +37,39 @@ void ConcurrentTask::executeTask()
{
m_total_size = m_queue.size();
- for (int i = 0; i < m_total_max_size; i++)
- startNext();
+ int num_starts = std::min(m_total_max_size, m_total_size);
+ for (int i = 0; i < num_starts; i++) {
+ QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection);
+ }
}
bool ConcurrentTask::abort()
{
+ m_queue.clear();
+ m_aborted = true;
+
if (m_doing.isEmpty()) {
// Don't call emitAborted() here, we want to bypass the 'is the task running' check
emit aborted();
emit finished();
- m_aborted = true;
return true;
}
- m_queue.clear();
+ bool suceedeed = true;
- m_aborted = true;
- for (auto task : m_doing)
- m_aborted &= task->abort();
+ QMutableHashIterator<Task*, Task::Ptr> doing_iter(m_doing);
+ while (doing_iter.hasNext()) {
+ auto task = doing_iter.next();
+ suceedeed &= (task.value())->abort();
+ }
- if (m_aborted)
+ if (suceedeed)
emitAborted();
+ else
+ emitFailed(tr("Failed to abort all running tasks."));
- return m_aborted;
+ return suceedeed;
}
void ConcurrentTask::startNext()
@@ -68,7 +77,7 @@ void ConcurrentTask::startNext()
if (m_aborted || m_doing.count() > m_total_max_size)
return;
- if (m_queue.isEmpty() && m_doing.isEmpty()) {
+ if (m_queue.isEmpty() && m_doing.isEmpty() && !wasSuccessful()) {
emitSucceeded();
return;
}
@@ -91,6 +100,8 @@ void ConcurrentTask::startNext()
setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus());
updateState();
+ QCoreApplication::processEvents();
+
next->start();
}
@@ -127,11 +138,6 @@ void ConcurrentTask::subTaskStatus(const QString& msg)
void ConcurrentTask::subTaskProgress(qint64 current, qint64 total)
{
- if (total == 0) {
- setProgress(0, 100);
- return;
- }
-
m_stepProgress = current;
m_stepTotalProgress = total;
}
diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h
index 5898899d..f1279d32 100644
--- a/launcher/tasks/ConcurrentTask.h
+++ b/launcher/tasks/ConcurrentTask.h
@@ -9,7 +9,9 @@ class ConcurrentTask : public Task {
Q_OBJECT
public:
explicit ConcurrentTask(QObject* parent = nullptr, QString task_name = "", int max_concurrent = 6);
- virtual ~ConcurrentTask();
+ ~ConcurrentTask() override;
+
+ bool canAbort() const override { return true; }
inline auto isMultiStep() const -> bool override { return m_queue.size() > 1; };
auto getStepProgress() const -> qint64 override;
diff --git a/launcher/tasks/MultipleOptionsTask.cpp b/launcher/tasks/MultipleOptionsTask.cpp
index 6e853568..5ad6181f 100644
--- a/launcher/tasks/MultipleOptionsTask.cpp
+++ b/launcher/tasks/MultipleOptionsTask.cpp
@@ -6,43 +6,22 @@ MultipleOptionsTask::MultipleOptionsTask(QObject* parent, const QString& task_na
void MultipleOptionsTask::startNext()
{
- Task* previous = nullptr;
- if (m_currentIndex != -1) {
- previous = m_queue[m_currentIndex].get();
- disconnect(previous, 0, this, 0);
- }
-
- m_currentIndex++;
- if ((previous && previous->wasSuccessful())) {
+ if (m_done.size() != m_failed.size()) {
emitSucceeded();
return;
}
- Task::Ptr next = m_queue[m_currentIndex];
-
- connect(next.get(), &Task::failed, this, &MultipleOptionsTask::subTaskFailed);
- connect(next.get(), &Task::succeeded, this, &MultipleOptionsTask::startNext);
-
- connect(next.get(), &Task::status, this, &MultipleOptionsTask::subTaskStatus);
- connect(next.get(), &Task::stepStatus, this, &MultipleOptionsTask::subTaskStatus);
-
- connect(next.get(), &Task::progress, this, &MultipleOptionsTask::subTaskProgress);
-
- qDebug() << QString("Making attemp %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size());
- setStatus(tr("Making attempt #%1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size()));
- setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus());
+ if (m_queue.isEmpty()) {
+ emitFailed(tr("All attempts have failed!"));
+ qWarning() << "All attempts have failed!";
+ return;
+ }
- next->start();
+ ConcurrentTask::startNext();
}
-void MultipleOptionsTask::subTaskFailed(QString const& reason)
+void MultipleOptionsTask::updateState()
{
- qDebug() << QString("Failed attempt #%1 of %2. Reason: %3").arg(m_currentIndex + 1).arg(m_queue.size()).arg(reason);
- if(m_currentIndex < m_queue.size() - 1) {
- startNext();
- return;
- }
-
- qWarning() << QString("All attempts have failed!");
- emitFailed();
+ setProgress(m_done.count(), m_total_size);
+ setStatus(tr("Attempting task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(m_total_size)));
}
diff --git a/launcher/tasks/MultipleOptionsTask.h b/launcher/tasks/MultipleOptionsTask.h
index 7c508b00..db7d4d9a 100644
--- a/launcher/tasks/MultipleOptionsTask.h
+++ b/launcher/tasks/MultipleOptionsTask.h
@@ -5,15 +5,13 @@
/* This task type will attempt to do run each of it's subtasks in sequence,
* until one of them succeeds. When that happens, the remaining tasks will not run.
* */
-class MultipleOptionsTask : public SequentialTask
-{
+class MultipleOptionsTask : public SequentialTask {
Q_OBJECT
-public:
- explicit MultipleOptionsTask(QObject *parent = nullptr, const QString& task_name = "");
- virtual ~MultipleOptionsTask() = default;
+ public:
+ explicit MultipleOptionsTask(QObject* parent = nullptr, const QString& task_name = "");
+ ~MultipleOptionsTask() override = default;
-private
-slots:
+ private slots:
void startNext() override;
- void subTaskFailed(const QString &msg) override;
+ void updateState() override;
};
diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp
index f1e1a889..a34137cb 100644
--- a/launcher/tasks/SequentialTask.cpp
+++ b/launcher/tasks/SequentialTask.cpp
@@ -2,107 +2,21 @@
#include <QDebug>
-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)
-{
- m_queue.append(task);
-}
-
-void SequentialTask::executeTask()
-{
- m_currentIndex = -1;
- startNext();
-}
-
-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_aborted = true;
- return true;
- }
-
- bool succeeded = m_queue[m_currentIndex]->abort();
- m_aborted = succeeded;
-
- if (succeeded)
- emitAborted();
-
- return succeeded;
-}
+SequentialTask::SequentialTask(QObject* parent, QString task_name) : ConcurrentTask(parent, task_name, 1) {}
void SequentialTask::startNext()
{
- if (m_aborted)
- return;
-
- if (m_currentIndex != -1 && m_currentIndex < m_queue.size()) {
- Task::Ptr previous = m_queue.at(m_currentIndex);
- disconnect(previous.get(), 0, this, 0);
- }
-
- m_currentIndex++;
- if (m_queue.isEmpty() || m_currentIndex >= m_queue.size()) {
- emitSucceeded();
+ if (m_failed.size() > 0) {
+ emitFailed(tr("One of the tasks failed!"));
+ qWarning() << m_failed.constBegin()->get()->failReason();
return;
}
- Task::Ptr next = m_queue[m_currentIndex];
-
- connect(next.get(), SIGNAL(failed(QString)), this, SLOT(subTaskFailed(QString)));
- 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());
-
- setProgress(m_currentIndex + 1, m_queue.count());
-
- next->start();
+ ConcurrentTask::startNext();
}
-void SequentialTask::subTaskFailed(const QString& msg)
+void SequentialTask::updateState()
{
- emitFailed(msg);
-}
-void SequentialTask::subTaskStatus(const QString& msg)
-{
- setStepStatus(msg);
-}
-void SequentialTask::subTaskProgress(qint64 current, qint64 total)
-{
- if (total == 0) {
- setProgress(0, 100);
- return;
- }
-
- m_stepProgress = current;
- m_stepTotalProgress = total;
+ setProgress(m_done.count(), m_total_size);
+ setStatus(tr("Executing task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(m_total_size)));
}
diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h
index f5a58b1b..5eace96e 100644
--- a/launcher/tasks/SequentialTask.h
+++ b/launcher/tasks/SequentialTask.h
@@ -1,49 +1,21 @@
#pragma once
-#include "Task.h"
-#include "QObjectPtr.h"
-
-#include <QQueue>
-
-class SequentialTask : public Task
-{
+#include "ConcurrentTask.h"
+
+/** A concurrent task that only allows one concurrent task :)
+ *
+ * This should be used when there's a need to maintain a strict ordering of task executions, and
+ * the starting of a task is contingent on the success of the previous one.
+ *
+ * See MultipleOptionsTask if that's not the case.
+ */
+class SequentialTask : public ConcurrentTask {
Q_OBJECT
-public:
- 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);
-
-public slots:
- bool abort() override;
-
-protected
-slots:
- void executeTask() override;
-
- virtual void startNext();
- virtual void subTaskFailed(const QString &msg);
- virtual void subTaskStatus(const QString &msg);
- virtual void subTaskProgress(qint64 current, qint64 total);
-
-protected:
- void setStepStatus(QString status) { m_step_status = status; emit stepStatus(status); };
-
-protected:
- QString m_name;
- QString m_step_status;
-
- QQueue<Task::Ptr > m_queue;
- int m_currentIndex;
-
- qint64 m_stepProgress = 0;
- qint64 m_stepTotalProgress = 100;
+ public:
+ explicit SequentialTask(QObject* parent = nullptr, QString task_name = "");
+ ~SequentialTask() override = default;
- bool m_aborted = false;
+ protected:
+ void startNext() override;
+ void updateState() override;
};
diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp
index bb71b98c..b4babdd4 100644
--- a/launcher/tasks/Task.cpp
+++ b/launcher/tasks/Task.cpp
@@ -37,8 +37,9 @@
#include <QDebug>
-Task::Task(QObject *parent) : QObject(parent)
+Task::Task(QObject *parent, bool show_debug) : QObject(parent), m_show_debug(show_debug)
{
+ setAutoDelete(false);
}
void Task::setStatus(const QString &new_status)
@@ -63,27 +64,32 @@ void Task::start()
{
case State::Inactive:
{
- qDebug() << "Task" << describe() << "starting for the first time";
+ if (m_show_debug)
+ qDebug() << "Task" << describe() << "starting for the first time";
break;
}
case State::AbortedByUser:
{
- qDebug() << "Task" << describe() << "restarting for after being aborted by user";
+ if (m_show_debug)
+ qDebug() << "Task" << describe() << "restarting for after being aborted by user";
break;
}
case State::Failed:
{
- qDebug() << "Task" << describe() << "restarting for after failing at first";
+ if (m_show_debug)
+ qDebug() << "Task" << describe() << "restarting for after failing at first";
break;
}
case State::Succeeded:
{
- qDebug() << "Task" << describe() << "restarting for after succeeding at first";
+ if (m_show_debug)
+ qDebug() << "Task" << describe() << "restarting for after succeeding at first";
break;
}
case State::Running:
{
- qWarning() << "The launcher tried to start task" << describe() << "while it was already running!";
+ if (m_show_debug)
+ qWarning() << "The launcher tried to start task" << describe() << "while it was already running!";
return;
}
}
@@ -118,7 +124,8 @@ void Task::emitAborted()
}
m_state = State::AbortedByUser;
m_failReason = "Aborted.";
- qDebug() << "Task" << describe() << "aborted.";
+ if (m_show_debug)
+ qDebug() << "Task" << describe() << "aborted.";
emit aborted();
emit finished();
}
@@ -132,7 +139,8 @@ void Task::emitSucceeded()
return;
}
m_state = State::Succeeded;
- qDebug() << "Task" << describe() << "succeeded";
+ if (m_show_debug)
+ qDebug() << "Task" << describe() << "succeeded";
emit succeeded();
emit finished();
}
diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h
index aafaf68c..2baf0188 100644
--- a/launcher/tasks/Task.h
+++ b/launcher/tasks/Task.h
@@ -35,9 +35,11 @@
#pragma once
+#include <QRunnable>
+
#include "QObjectPtr.h"
-class Task : public QObject {
+class Task : public QObject, public QRunnable {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<Task>;
@@ -45,7 +47,7 @@ class Task : public QObject {
enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser };
public:
- explicit Task(QObject* parent = 0);
+ explicit Task(QObject* parent = 0, bool show_debug_log = true);
virtual ~Task() = default;
bool isRunning() const;
@@ -95,6 +97,9 @@ class Task : public QObject {
void stepStatus(QString status);
public slots:
+ // QRunnable's interface
+ void run() override { start(); }
+
virtual void start();
virtual bool abort() { if(canAbort()) emitAborted(); return canAbort(); };
@@ -117,4 +122,7 @@ class Task : public QObject {
QString m_status;
int m_progress = 0;
int m_progressTotal = 100;
+
+ // TODO: Nuke in favor of QLoggingCategory
+ bool m_show_debug = true;
};
diff --git a/launcher/tasks/Task_test.cpp b/launcher/tasks/Task_test.cpp
index ef153a6a..b56ee8a6 100644
--- a/launcher/tasks/Task_test.cpp
+++ b/launcher/tasks/Task_test.cpp
@@ -1,5 +1,8 @@
#include <QTest>
+#include "ConcurrentTask.h"
+#include "MultipleOptionsTask.h"
+#include "SequentialTask.h"
#include "Task.h"
/* Does nothing. Only used for testing. */
@@ -9,7 +12,10 @@ class BasicTask : public Task {
friend class TaskTest;
private:
- void executeTask() override {};
+ void executeTask() override
+ {
+ emitSucceeded();
+ };
};
/* Does nothing. Only used for testing. */
@@ -60,6 +66,123 @@ class TaskTest : public QObject {
QCOMPARE(t.getProgress(), current);
QCOMPARE(t.getTotalProgress(), total);
}
+
+ void test_basicRun(){
+ BasicTask t;
+ QObject::connect(&t, &Task::finished, [&]{ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); });
+ t.start();
+
+ QVERIFY2(QTest::qWaitFor([&]() {
+ return t.isFinished();
+ }, 1000), "Task didn't finish as it should.");
+ }
+
+ void test_basicConcurrentRun(){
+ BasicTask t1;
+ BasicTask t2;
+ BasicTask t3;
+
+ ConcurrentTask t;
+
+ t.addTask(&t1);
+ t.addTask(&t2);
+ t.addTask(&t3);
+
+ QObject::connect(&t, &Task::finished, [&]{
+ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
+ QVERIFY(t1.wasSuccessful());
+ QVERIFY(t2.wasSuccessful());
+ QVERIFY(t3.wasSuccessful());
+ });
+
+ t.start();
+ QVERIFY2(QTest::qWaitFor([&]() {
+ return t.isFinished();
+ }, 1000), "Task didn't finish as it should.");
+ }
+
+ // Tests if starting new tasks after the 6 initial ones is working
+ void test_moreConcurrentRun(){
+ BasicTask t1, t2, t3, t4, t5, t6, t7, t8, t9;
+
+ ConcurrentTask t;
+
+ t.addTask(&t1);
+ t.addTask(&t2);
+ t.addTask(&t3);
+ t.addTask(&t4);
+ t.addTask(&t5);
+ t.addTask(&t6);
+ t.addTask(&t7);
+ t.addTask(&t8);
+ t.addTask(&t9);
+
+ QObject::connect(&t, &Task::finished, [&]{
+ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
+ QVERIFY(t1.wasSuccessful());
+ QVERIFY(t2.wasSuccessful());
+ QVERIFY(t3.wasSuccessful());
+ QVERIFY(t4.wasSuccessful());
+ QVERIFY(t5.wasSuccessful());
+ QVERIFY(t6.wasSuccessful());
+ QVERIFY(t7.wasSuccessful());
+ QVERIFY(t8.wasSuccessful());
+ QVERIFY(t9.wasSuccessful());
+ });
+
+ t.start();
+ QVERIFY2(QTest::qWaitFor([&]() {
+ return t.isFinished();
+ }, 1000), "Task didn't finish as it should.");
+ }
+
+ void test_basicSequentialRun(){
+ BasicTask t1;
+ BasicTask t2;
+ BasicTask t3;
+
+ SequentialTask t;
+
+ t.addTask(&t1);
+ t.addTask(&t2);
+ t.addTask(&t3);
+
+ QObject::connect(&t, &Task::finished, [&]{
+ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
+ QVERIFY(t1.wasSuccessful());
+ QVERIFY(t2.wasSuccessful());
+ QVERIFY(t3.wasSuccessful());
+ });
+
+ t.start();
+ QVERIFY2(QTest::qWaitFor([&]() {
+ return t.isFinished();
+ }, 1000), "Task didn't finish as it should.");
+ }
+
+ void test_basicMultipleOptionsRun(){
+ BasicTask t1;
+ BasicTask t2;
+ BasicTask t3;
+
+ MultipleOptionsTask t;
+
+ t.addTask(&t1);
+ t.addTask(&t2);
+ t.addTask(&t3);
+
+ QObject::connect(&t, &Task::finished, [&]{
+ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
+ QVERIFY(t1.wasSuccessful());
+ QVERIFY(!t2.wasSuccessful());
+ QVERIFY(!t3.wasSuccessful());
+ });
+
+ t.start();
+ QVERIFY2(QTest::qWaitFor([&]() {
+ return t.isFinished();
+ }, 1000), "Task didn't finish as it should.");
+ }
};
QTEST_GUILESS_MAIN(TaskTest)
diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp
index c3d95599..299401f5 100644
--- a/launcher/ui/MainWindow.cpp
+++ b/launcher/ui/MainWindow.cpp
@@ -1465,6 +1465,7 @@ void MainWindow::updateNewsLabel()
{
newsLabel->setText(tr("Loading news..."));
newsLabel->setEnabled(false);
+ ui->actionMoreNews->setVisible(false);
}
else
{
@@ -1473,11 +1474,13 @@ void MainWindow::updateNewsLabel()
{
newsLabel->setText(entries[0]->title);
newsLabel->setEnabled(true);
+ ui->actionMoreNews->setVisible(true);
}
else
{
newsLabel->setText(tr("No news available."));
newsLabel->setEnabled(false);
+ ui->actionMoreNews->setVisible(false);
}
}
}
diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp
index c5367d5b..743c34f1 100644
--- a/launcher/ui/dialogs/AboutDialog.cpp
+++ b/launcher/ui/dialogs/AboutDialog.cpp
@@ -147,10 +147,15 @@ AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), ui(new Ui::AboutDia
else
ui->platformLabel->setVisible(false);
- if (BuildConfig.VERSION_BUILD >= 0)
- ui->buildNumLabel->setText(tr("Build Number") +": " + QString::number(BuildConfig.VERSION_BUILD));
+ if (!BuildConfig.GIT_COMMIT.isEmpty())
+ ui->commitLabel->setText(tr("Commit: %1").arg(BuildConfig.GIT_COMMIT));
else
- ui->buildNumLabel->setVisible(false);
+ ui->commitLabel->setVisible(false);
+
+ if (!BuildConfig.BUILD_DATE.isEmpty())
+ ui->buildDateLabel->setText(tr("Build date: %1").arg(BuildConfig.BUILD_DATE));
+ else
+ ui->buildDateLabel->setVisible(false);
if (!BuildConfig.VERSION_CHANNEL.isEmpty())
ui->channelLabel->setText(tr("Channel") +": " + BuildConfig.VERSION_CHANNEL);
diff --git a/launcher/ui/dialogs/AboutDialog.ui b/launcher/ui/dialogs/AboutDialog.ui
index 6323992b..e0429321 100644
--- a/launcher/ui/dialogs/AboutDialog.ui
+++ b/launcher/ui/dialogs/AboutDialog.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>783</width>
- <height>843</height>
+ <width>573</width>
+ <height>600</height>
</rect>
</property>
<property name="minimumSize">
@@ -184,12 +184,28 @@
</widget>
</item>
<item>
- <widget class="QLabel" name="buildNumLabel">
+ <widget class="QLabel" name="buildDateLabel">
<property name="cursor">
<cursorShape>IBeamCursor</cursorShape>
</property>
<property name="text">
- <string>Build Number:</string>
+ <string>Build Date:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="commitLabel">
+ <property name="cursor">
+ <cursorShape>IBeamCursor</cursorShape>
+ </property>
+ <property name="text">
+ <string>Commit:</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp
new file mode 100644
index 00000000..fe87b517
--- /dev/null
+++ b/launcher/ui/dialogs/BlockedModsDialog.cpp
@@ -0,0 +1,28 @@
+#include "BlockedModsDialog.h"
+#include "ui_BlockedModsDialog.h"
+#include <QPushButton>
+#include <QDialogButtonBox>
+#include <QDesktopServices>
+
+
+BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, const QString &body, const QList<QUrl> &urls) :
+ QDialog(parent), ui(new Ui::BlockedModsDialog), urls(urls) {
+ ui->setupUi(this);
+
+ auto openAllButton = ui->buttonBox->addButton(tr("Open All"), QDialogButtonBox::ActionRole);
+ connect(openAllButton, &QPushButton::clicked, this, &BlockedModsDialog::openAll);
+
+ this->setWindowTitle(title);
+ ui->label->setText(text);
+ ui->textBrowser->setText(body);
+}
+
+BlockedModsDialog::~BlockedModsDialog() {
+ delete ui;
+}
+
+void BlockedModsDialog::openAll() {
+ for(auto &url : urls) {
+ QDesktopServices::openUrl(url);
+ }
+}
diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h
new file mode 100644
index 00000000..5f5bd61b
--- /dev/null
+++ b/launcher/ui/dialogs/BlockedModsDialog.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <QDialog>
+
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class BlockedModsDialog; }
+QT_END_NAMESPACE
+
+class BlockedModsDialog : public QDialog {
+Q_OBJECT
+
+public:
+ BlockedModsDialog(QWidget *parent, const QString &title, const QString &text, const QString &body, const QList<QUrl> &urls);
+
+ ~BlockedModsDialog() override;
+
+private:
+ Ui::BlockedModsDialog *ui;
+ const QList<QUrl> &urls;
+ void openAll();
+};
diff --git a/launcher/ui/dialogs/BlockedModsDialog.ui b/launcher/ui/dialogs/BlockedModsDialog.ui
new file mode 100644
index 00000000..f4ae95b6
--- /dev/null
+++ b/launcher/ui/dialogs/BlockedModsDialog.ui
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>BlockedModsDialog</class>
+ <widget class="QDialog" name="BlockedModsDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>455</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string notr="true">BlockedModsDialog</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QTextBrowser" name="textBrowser">
+ <property name="acceptRichText">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>BlockedModsDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>199</x>
+ <y>425</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>199</x>
+ <y>227</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>BlockedModsDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>199</x>
+ <y>425</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>199</x>
+ <y>227</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/ui/dialogs/LoginDialog.cpp b/launcher/ui/dialogs/LoginDialog.cpp
index 194315a7..30394b72 100644
--- a/launcher/ui/dialogs/LoginDialog.cpp
+++ b/launcher/ui/dialogs/LoginDialog.cpp
@@ -115,5 +115,5 @@ MinecraftAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg)
{
return dlg.m_account;
}
- return 0;
+ return nullptr;
}
diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp
index b11b6980..be49babb 100644
--- a/launcher/ui/dialogs/MSALoginDialog.cpp
+++ b/launcher/ui/dialogs/MSALoginDialog.cpp
@@ -169,5 +169,5 @@ MinecraftAccountPtr MSALoginDialog::newAccount(QWidget *parent, QString msg)
{
return dlg.m_account;
}
- return 0;
+ return nullptr;
}
diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp
index 05f76fbb..7382d1cf 100644
--- a/launcher/ui/dialogs/ModDownloadDialog.cpp
+++ b/launcher/ui/dialogs/ModDownloadDialog.cpp
@@ -128,7 +128,7 @@ QList<BasePage*> ModDownloadDialog::getPages()
QList<BasePage*> pages;
pages.append(new ModrinthModPage(this, m_instance));
- if (APPLICATION->currentCapabilities() & Application::SupportsFlame)
+ if (APPLICATION->capabilities() & Application::SupportsFlame)
pages.append(new FlameModPage(this, m_instance));
return pages;
diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp
index d73c8ebb..4171586e 100644
--- a/launcher/ui/dialogs/ModUpdateDialog.cpp
+++ b/launcher/ui/dialogs/ModUpdateDialog.cpp
@@ -36,7 +36,7 @@ static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst)
ModUpdateDialog::ModUpdateDialog(QWidget* parent,
BaseInstance* instance,
const std::shared_ptr<ModFolderModel> mods,
- QList<Mod::Ptr>& search_for)
+ QList<Mod*>& search_for)
: ReviewMessageBox(parent, tr("Confirm mods to update"), "")
, m_parent(parent)
, m_mod_model(mods)
@@ -226,9 +226,8 @@ auto ModUpdateDialog::ensureMetadata() -> bool
};
for (auto candidate : m_candidates) {
- auto* candidate_ptr = candidate.get();
if (candidate->status() != ModStatus::NoMetadata) {
- onMetadataEnsured(candidate_ptr);
+ onMetadataEnsured(candidate);
continue;
}
@@ -236,7 +235,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool
continue;
if (confirm_rest) {
- addToTmp(candidate_ptr, provider_rest);
+ addToTmp(candidate, provider_rest);
should_try_others.insert(candidate->internal_id(), try_others_rest);
continue;
}
@@ -261,7 +260,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool
should_try_others.insert(candidate->internal_id(), response.try_others);
if (confirmed)
- addToTmp(candidate_ptr, response.chosen);
+ addToTmp(candidate, response.chosen);
}
if (!modrinth_tmp.empty()) {
@@ -270,6 +269,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool
connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::MODRINTH);
});
+
+ if (modrinth_task->getHashingTask())
+ seq.addTask(modrinth_task->getHashingTask());
+
seq.addTask(modrinth_task);
}
@@ -279,6 +282,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool
connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::FLAME);
});
+
+ if (flame_task->getHashingTask())
+ seq.addTask(flame_task->getHashingTask());
+
seq.addTask(flame_task);
}
diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h
index 76aaab36..bd486f0d 100644
--- a/launcher/ui/dialogs/ModUpdateDialog.h
+++ b/launcher/ui/dialogs/ModUpdateDialog.h
@@ -19,7 +19,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
explicit ModUpdateDialog(QWidget* parent,
BaseInstance* instance,
const std::shared_ptr<ModFolderModel> mod_model,
- QList<Mod::Ptr>& search_for);
+ QList<Mod*>& search_for);
void checkCandidates();
@@ -46,7 +46,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
const std::shared_ptr<ModFolderModel> m_mod_model;
- QList<Mod::Ptr>& m_candidates;
+ QList<Mod*>& m_candidates;
QList<Mod*> m_modrinth_to_update;
QList<Mod*> m_flame_to_update;
diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp
index 35bba9be..675f8b15 100644
--- a/launcher/ui/dialogs/NewInstanceDialog.cpp
+++ b/launcher/ui/dialogs/NewInstanceDialog.cpp
@@ -157,7 +157,7 @@ QList<BasePage *> NewInstanceDialog::getPages()
pages.append(new VanillaPage(this));
pages.append(importPage);
pages.append(new AtlPage(this));
- if (APPLICATION->currentCapabilities() & Application::SupportsFlame)
+ if (APPLICATION->capabilities() & Application::SupportsFlame)
pages.append(new FlamePage(this));
pages.append(new FtbPage(this));
pages.append(new LegacyFTB::Page(this));
diff --git a/launcher/ui/dialogs/OfflineLoginDialog.cpp b/launcher/ui/dialogs/OfflineLoginDialog.cpp
index 4f3d8be4..a69537ab 100644
--- a/launcher/ui/dialogs/OfflineLoginDialog.cpp
+++ b/launcher/ui/dialogs/OfflineLoginDialog.cpp
@@ -103,5 +103,5 @@ MinecraftAccountPtr OfflineLoginDialog::newAccount(QWidget *parent, QString msg)
{
return dlg.m_account;
}
- return 0;
+ return nullptr;
}
diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp
index fcc43add..a4f4dfb9 100644
--- a/launcher/ui/pages/global/AccountListPage.cpp
+++ b/launcher/ui/pages/global/AccountListPage.cpp
@@ -96,7 +96,7 @@ AccountListPage::AccountListPage(QWidget *parent)
updateButtonStates();
// Xbox authentication won't work without a client identifier, so disable the button if it is missing
- if (~APPLICATION->currentCapabilities() & Application::SupportsMSA) {
+ if (~APPLICATION->capabilities() & Application::SupportsMSA) {
ui->actionAddMicrosoft->setVisible(false);
ui->actionAddMicrosoft->setToolTip(tr("No Microsoft Authentication client ID was set."));
}
diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp
index e3ac7e7c..cc597fe0 100644
--- a/launcher/ui/pages/global/MinecraftPage.cpp
+++ b/launcher/ui/pages/global/MinecraftPage.cpp
@@ -122,6 +122,16 @@ void MinecraftPage::loadSettings()
ui->perfomanceGroupBox->setVisible(false);
#endif
+ if (!(APPLICATION->capabilities() & Application::SupportsGameMode)) {
+ ui->enableFeralGamemodeCheck->setDisabled(true);
+ ui->enableFeralGamemodeCheck->setToolTip(tr("Feral Interactive's GameMode could not be found on your system."));
+ }
+
+ if (!(APPLICATION->capabilities() & Application::SupportsMangoHud)) {
+ ui->enableMangoHud->setDisabled(true);
+ ui->enableMangoHud->setToolTip(tr("MangoHud could not be found on your system."));
+ }
+
ui->showGameTime->setChecked(s->get("ShowGameTime").toBool());
ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool());
ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool());
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
index 69c20309..f31e8325 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
@@ -3,109 +3,22 @@
#include "DesktopServices.h"
#include "Version.h"
-#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/mod/ResourceFolderModel.h"
#include "ui/GuiUtil.h"
#include <QKeyEvent>
#include <QMenu>
-namespace {
-// FIXME: wasteful
-void RemoveThePrefix(QString& string)
-{
- QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption);
- string.remove(regex);
- string = string.trimmed();
-}
-} // namespace
-
-class SortProxy : public QSortFilterProxyModel {
- public:
- explicit SortProxy(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {}
-
- protected:
- bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override
- {
- ModFolderModel* model = qobject_cast<ModFolderModel*>(sourceModel());
- if (!model)
- return false;
-
- const auto& mod = model->at(source_row);
-
- if (filterRegularExpression().match(mod.name()).hasMatch())
- return true;
- if (filterRegularExpression().match(mod.description()).hasMatch())
- return true;
-
- for (auto& author : mod.authors()) {
- if (filterRegularExpression().match(author).hasMatch()) {
- return true;
- }
- }
-
- return false;
- }
-
- bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override
- {
- ModFolderModel* model = qobject_cast<ModFolderModel*>(sourceModel());
- if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) {
- return QSortFilterProxyModel::lessThan(source_left, source_right);
- }
-
- // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and
- // proceed.
-
- auto column = (ModFolderModel::Columns) source_left.column();
- bool invert = false;
- switch (column) {
- // GH-2550 - sort by enabled/disabled
- case ModFolderModel::ActiveColumn: {
- auto dataL = source_left.data(Qt::CheckStateRole).toBool();
- auto dataR = source_right.data(Qt::CheckStateRole).toBool();
- if (dataL != dataR)
- return dataL > dataR;
-
- // fallthrough
- invert = sortOrder() == Qt::DescendingOrder;
- }
- // GH-2722 - sort mod names in a way that discards "The" prefixes
- case ModFolderModel::NameColumn: {
- auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString();
- RemoveThePrefix(dataL);
- auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString();
- RemoveThePrefix(dataR);
-
- auto less = dataL.compare(dataR, sortCaseSensitivity());
- if (less != 0)
- return invert ? (less > 0) : (less < 0);
-
- // fallthrough
- invert = sortOrder() == Qt::DescendingOrder;
- }
- // GH-2762 - sort versions by parsing them as versions
- case ModFolderModel::VersionColumn: {
- auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString());
- auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString());
- return invert ? (dataL > dataR) : (dataL < dataR);
- }
- default: {
- return QSortFilterProxyModel::lessThan(source_left, source_right);
- }
- }
- }
-};
-
-ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ModFolderModel> model, QWidget* parent)
+ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ResourceFolderModel> model, QWidget* parent)
: QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model)
{
ui->setupUi(this);
- runningStateChanged(m_instance && m_instance->isRunning());
+ ExternalResourcesPage::runningStateChanged(m_instance && m_instance->isRunning());
ui->actionsToolbar->insertSpacer(ui->actionViewConfigs);
- m_filterModel = new SortProxy(this);
+ m_filterModel = model->createFilterProxyModel(this);
m_filterModel->setDynamicSortFilter(true);
m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
@@ -137,19 +50,9 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
ExternalResourcesPage::~ExternalResourcesPage()
{
- m_model->stopWatching();
delete ui;
}
-void ExternalResourcesPage::itemActivated(const QModelIndex&)
-{
- if (!m_controlsEnabled)
- return;
-
- auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
- m_model->setModStatus(selection.indexes(), ModFolderModel::Toggle);
-}
-
QMenu* ExternalResourcesPage::createPopupMenu()
{
QMenu* filteredMenu = QMainWindow::createPopupMenu();
@@ -179,6 +82,15 @@ void ExternalResourcesPage::retranslate()
ui->retranslateUi(this);
}
+void ExternalResourcesPage::itemActivated(const QModelIndex&)
+{
+ if (!m_controlsEnabled)
+ return;
+
+ auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
+ m_model->setResourceEnabled(selection.indexes(), EnableAction::TOGGLE);
+}
+
void ExternalResourcesPage::filterTextChanged(const QString& newContents)
{
m_viewFilter = newContents;
@@ -241,7 +153,7 @@ void ExternalResourcesPage::addItem()
if (!list.isEmpty()) {
for (auto filename : list) {
- m_model->installMod(filename);
+ m_model->installResource(filename);
}
}
}
@@ -252,25 +164,25 @@ void ExternalResourcesPage::removeItem()
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
- m_model->deleteMods(selection.indexes());
+ m_model->deleteResources(selection.indexes());
}
void ExternalResourcesPage::enableItem()
{
if (!m_controlsEnabled)
return;
-
+
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
- m_model->setModStatus(selection.indexes(), ModFolderModel::Enable);
+ m_model->setResourceEnabled(selection.indexes(), EnableAction::ENABLE);
}
void ExternalResourcesPage::disableItem()
{
if (!m_controlsEnabled)
return;
-
+
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
- m_model->setModStatus(selection.indexes(), ModFolderModel::Disable);
+ m_model->setResourceEnabled(selection.indexes(), EnableAction::DISABLE);
}
void ExternalResourcesPage::viewConfigs()
@@ -283,15 +195,23 @@ void ExternalResourcesPage::viewFolder()
DesktopServices::openDirectory(m_model->dir().absolutePath(), true);
}
-void ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous)
+bool ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous)
{
if (!current.isValid()) {
ui->frame->clear();
- return;
+ return false;
}
+ return onSelectionChanged(current, previous);
+}
+
+bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous)
+{
auto sourceCurrent = m_filterModel->mapToSource(current);
int row = sourceCurrent.row();
- Mod& m = m_model->operator[](row);
- ui->frame->updateWithMod(m);
+ Resource const& resource = m_model->at(row);
+ ui->frame->updateWithResource(resource);
+
+ return true;
}
+
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h
index 41237139..8e352cef 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.h
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.h
@@ -7,7 +7,7 @@
#include "minecraft/MinecraftInstance.h"
#include "ui/pages/BasePage.h"
-class ModFolderModel;
+class ResourceFolderModel;
namespace Ui {
class ExternalResourcesPage;
@@ -19,8 +19,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
Q_OBJECT
public:
- // FIXME: Switch to different model (or change the name of this one)
- explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ModFolderModel> model, QWidget* parent = nullptr);
+ explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ResourceFolderModel> model, QWidget* parent = nullptr);
virtual ~ExternalResourcesPage();
virtual QString displayName() const override = 0;
@@ -41,12 +40,14 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
QMenu* createPopupMenu() override;
public slots:
- void current(const QModelIndex& current, const QModelIndex& previous);
+ bool current(const QModelIndex& current, const QModelIndex& previous);
+
+ virtual bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous);
protected slots:
void itemActivated(const QModelIndex& index);
void filterTextChanged(const QString& newContents);
- void runningStateChanged(bool running);
+ virtual void runningStateChanged(bool running);
virtual void addItem();
virtual void removeItem();
@@ -63,7 +64,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
BaseInstance* m_instance = nullptr;
Ui::ExternalResourcesPage* ui = nullptr;
- std::shared_ptr<ModFolderModel> m_model;
+ std::shared_ptr<ResourceFolderModel> m_model;
QSortFilterProxyModel* m_filterModel = nullptr;
QString m_fileSelectionFilter;
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui
index a13666b2..76f8ec18 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.ui
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui
@@ -43,7 +43,7 @@
</layout>
</item>
<item row="2" column="1" colspan="3">
- <widget class="MCModInfoFrame" name="frame">
+ <widget class="InfoFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
@@ -166,9 +166,9 @@
<header>ui/widgets/ModListView.h</header>
</customwidget>
<customwidget>
- <class>MCModInfoFrame</class>
+ <class>InfoFrame</class>
<extends>QFrame</extends>
- <header>ui/widgets/MCModInfoFrame.h</header>
+ <header>ui/widgets/InfoFrame.h</header>
<container>1</container>
</customwidget>
<customwidget>
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
index f11cf992..03910745 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
@@ -348,9 +348,19 @@ void InstanceSettingsPage::loadSettings()
ui->enableMangoHud->setChecked(m_settings->get("EnableMangoHud").toBool());
ui->useDiscreteGpuCheck->setChecked(m_settings->get("UseDiscreteGpu").toBool());
- #if !defined(Q_OS_LINUX)
+#if !defined(Q_OS_LINUX)
ui->settingsTabs->setTabVisible(ui->settingsTabs->indexOf(ui->performancePage), false);
- #endif
+#endif
+
+ if (!(APPLICATION->capabilities() & Application::SupportsGameMode)) {
+ ui->enableFeralGamemodeCheck->setDisabled(true);
+ ui->enableFeralGamemodeCheck->setToolTip(tr("Feral Interactive's GameMode could not be found on your system."));
+ }
+
+ if (!(APPLICATION->capabilities() & Application::SupportsMangoHud)) {
+ ui->enableMangoHud->setDisabled(true);
+ ui->enableMangoHud->setToolTip(tr("MangoHud could not be found on your system."));
+ }
// Miscellanous
ui->gameTimeGroupBox->setChecked(m_settings->get("OverrideGameTime").toBool());
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp
index 14e1f1e5..75b40e77 100644
--- a/launcher/ui/pages/instance/ModFolderPage.cpp
+++ b/launcher/ui/pages/instance/ModFolderPage.cpp
@@ -65,7 +65,7 @@
#include "ui/dialogs/ProgressDialog.h"
ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
- : ExternalResourcesPage(inst, mods, parent)
+ : ExternalResourcesPage(inst, mods, parent), m_model(mods)
{
// This is structured like that so that these changes
// do not affect the Resource pack and Shader pack tabs
@@ -84,49 +84,55 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem);
connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods);
- connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
- [this] { ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); });
+ auto check_allow_update = [this] {
+ return (!m_instance || !m_instance->isRunning()) &&
+ (ui->treeView->selectionModel()->hasSelection() || !m_model->empty());
+ };
- connect(mods.get(), &ModFolderModel::rowsInserted, this,
- [this] { ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); });
-
- connect(mods.get(), &ModFolderModel::updateFinished, this, [this, mods] {
- ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty());
+ connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] {
+ ui->actionUpdateItem->setEnabled(check_allow_update());
+ });
+
+ connect(mods.get(), &ModFolderModel::rowsInserted, this, [this, check_allow_update] {
+ ui->actionUpdateItem->setEnabled(check_allow_update());
+ });
+
+ connect(mods.get(), &ModFolderModel::rowsRemoved, this, [this, check_allow_update] {
+ ui->actionUpdateItem->setEnabled(check_allow_update());
+ });
+
+ connect(mods.get(), &ModFolderModel::updateFinished, this, [this, check_allow_update, mods] {
+ ui->actionUpdateItem->setEnabled(check_allow_update());
// Prevent a weird crash when trying to open the mods page twice in a session o.O
disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0);
});
+
+ ModFolderPage::runningStateChanged(m_instance && m_instance->isRunning());
}
}
-CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
- : ModFolderPage(inst, mods, parent)
-{}
+void ModFolderPage::runningStateChanged(bool running)
+{
+ ExternalResourcesPage::runningStateChanged(running);
+ ui->actionDownloadItem->setEnabled(!running);
+ ui->actionUpdateItem->setEnabled(!running);
+}
bool ModFolderPage::shouldDisplay() const
{
return true;
}
-bool CoreModFolderPage::shouldDisplay() const
+bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous)
{
- if (ModFolderPage::shouldDisplay()) {
- auto inst = dynamic_cast<MinecraftInstance*>(m_instance);
- if (!inst)
- return true;
+ auto sourceCurrent = m_filterModel->mapToSource(current);
+ int row = sourceCurrent.row();
+ Mod const* m = m_model->at(row);
+ if (m)
+ ui->frame->updateWithMod(*m);
- auto version = inst->getPackProfile();
-
- if (!version)
- return true;
- if (!version->getComponent("net.minecraftforge"))
- return false;
- if (!version->getComponent("net.minecraft"))
- return false;
- if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate)
- return true;
- }
- return false;
+ return true;
}
void ModFolderPage::installMods()
@@ -232,3 +238,28 @@ void ModFolderPage::updateMods()
m_model->update();
}
}
+
+CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
+ : ModFolderPage(inst, mods, parent)
+{}
+
+bool CoreModFolderPage::shouldDisplay() const
+{
+ if (ModFolderPage::shouldDisplay()) {
+ auto inst = dynamic_cast<MinecraftInstance*>(m_instance);
+ if (!inst)
+ return true;
+
+ auto version = inst->getPackProfile();
+
+ if (!version)
+ return true;
+ if (!version->getComponent("net.minecraftforge"))
+ return false;
+ if (!version->getComponent("net.minecraft"))
+ return false;
+ if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate)
+ return true;
+ }
+ return false;
+}
diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h
index 0a7fc9fa..7fc9d9a1 100644
--- a/launcher/ui/pages/instance/ModFolderPage.h
+++ b/launcher/ui/pages/instance/ModFolderPage.h
@@ -53,15 +53,28 @@ class ModFolderPage : public ExternalResourcesPage {
virtual QString helpPage() const override { return "Loader-mods"; }
virtual bool shouldDisplay() const override;
+ void runningStateChanged(bool running) override;
+
+ public slots:
+ bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override;
private slots:
void installMods();
void updateMods();
+
+ protected:
+ std::shared_ptr<ModFolderModel> m_model;
};
class CoreModFolderPage : public ModFolderPage {
public:
explicit CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent = 0);
virtual ~CoreModFolderPage() = default;
- virtual bool shouldDisplay() const;
+
+ virtual QString displayName() const override { return tr("Core mods"); }
+ virtual QIcon icon() const override { return APPLICATION->getThemedIcon("coremods"); }
+ virtual QString id() const override { return "coremods"; }
+ virtual QString helpPage() const override { return "Core-mods"; }
+
+ virtual bool shouldDisplay() const override;
};
diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h
index a6c9fdd3..2eefc3d3 100644
--- a/launcher/ui/pages/instance/ResourcePackPage.h
+++ b/launcher/ui/pages/instance/ResourcePackPage.h
@@ -38,12 +38,14 @@
#include "ExternalResourcesPage.h"
#include "ui_ExternalResourcesPage.h"
+#include "minecraft/mod/ResourcePackFolderModel.h"
+
class ResourcePackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
- explicit ResourcePackPage(MinecraftInstance *instance, QWidget *parent = 0)
- : ExternalResourcesPage(instance, instance->resourcePackList(), parent)
+ explicit ResourcePackPage(MinecraftInstance *instance, std::shared_ptr<ResourcePackFolderModel> model, QWidget *parent = 0)
+ : ExternalResourcesPage(instance, model, parent)
{
ui->actionViewConfigs->setVisible(false);
}
diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h
index 2cc056c8..7f7ff8c1 100644
--- a/launcher/ui/pages/instance/ShaderPackPage.h
+++ b/launcher/ui/pages/instance/ShaderPackPage.h
@@ -38,12 +38,14 @@
#include "ExternalResourcesPage.h"
#include "ui_ExternalResourcesPage.h"
+#include "minecraft/mod/ShaderPackFolderModel.h"
+
class ShaderPackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
- explicit ShaderPackPage(MinecraftInstance *instance, QWidget *parent = 0)
- : ExternalResourcesPage(instance, instance->shaderPackList(), parent)
+ explicit ShaderPackPage(MinecraftInstance *instance, std::shared_ptr<ShaderPackFolderModel> model, QWidget *parent = 0)
+ : ExternalResourcesPage(instance, model, parent)
{
ui->actionViewConfigs->setVisible(false);
}
diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h
index f550a5bc..fa219eda 100644
--- a/launcher/ui/pages/instance/TexturePackPage.h
+++ b/launcher/ui/pages/instance/TexturePackPage.h
@@ -38,12 +38,14 @@
#include "ExternalResourcesPage.h"
#include "ui_ExternalResourcesPage.h"
+#include "minecraft/mod/TexturePackFolderModel.h"
+
class TexturePackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
- explicit TexturePackPage(MinecraftInstance *instance, QWidget *parent = 0)
- : ExternalResourcesPage(instance, instance->texturePackList(), parent)
+ explicit TexturePackPage(MinecraftInstance *instance, std::shared_ptr<TexturePackFolderModel> model, QWidget *parent = 0)
+ : ExternalResourcesPage(instance, model, parent)
{
ui->actionViewConfigs->setVisible(false);
}
diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp
index 468ff35c..a021c633 100644
--- a/launcher/ui/pages/instance/VersionPage.cpp
+++ b/launcher/ui/pages/instance/VersionPage.cpp
@@ -196,10 +196,10 @@ void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &
switch(severity)
{
case ProblemSeverity::Warning:
- ui->frame->setModText(tr("%1 possibly has issues.").arg(patch->getName()));
+ ui->frame->setName(tr("%1 possibly has issues.").arg(patch->getName()));
break;
case ProblemSeverity::Error:
- ui->frame->setModText(tr("%1 has issues!").arg(patch->getName()));
+ ui->frame->setName(tr("%1 has issues!").arg(patch->getName()));
break;
default:
case ProblemSeverity::None:
@@ -222,7 +222,7 @@ void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &
problemOut += problem.m_description;
problemOut += "\n";
}
- ui->frame->setModDescription(problemOut);
+ ui->frame->setDescription(problemOut);
}
void VersionPage::updateRunningStatus(bool running)
diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui
index 489f7218..fcba5598 100644
--- a/launcher/ui/pages/instance/VersionPage.ui
+++ b/launcher/ui/pages/instance/VersionPage.ui
@@ -64,7 +64,7 @@
</layout>
</item>
<item>
- <widget class="MCModInfoFrame" name="frame">
+ <widget class="InfoFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
@@ -278,9 +278,9 @@
<header>ui/widgets/ModListView.h</header>
</customwidget>
<customwidget>
- <class>MCModInfoFrame</class>
+ <class>InfoFrame</class>
<extends>QFrame</extends>
- <header>ui/widgets/MCModInfoFrame.h</header>
+ <header>ui/widgets/InfoFrame.h</header>
<container>1</container>
</customwidget>
<customwidget>
diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp
index 647b04a7..85cc01ff 100644
--- a/launcher/ui/pages/instance/WorldListPage.cpp
+++ b/launcher/ui/pages/instance/WorldListPage.cpp
@@ -211,7 +211,7 @@ void WorldListPage::on_actionDatapacks_triggered()
return;
}
- if(!worldSafetyNagQuestion())
+ if(!worldSafetyNagQuestion(tr("Open World Datapacks Folder")))
return;
auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
@@ -269,7 +269,7 @@ void WorldListPage::on_actionMCEdit_triggered()
return;
}
- if(!worldSafetyNagQuestion())
+ if(!worldSafetyNagQuestion(tr("Open World in MCEdit")))
return;
auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
@@ -373,11 +373,11 @@ bool WorldListPage::isWorldSafe(QModelIndex)
return !m_inst->isRunning();
}
-bool WorldListPage::worldSafetyNagQuestion()
+bool WorldListPage::worldSafetyNagQuestion(const QString &actionType)
{
if(!isWorldSafe(getSelectedWorld()))
{
- auto result = QMessageBox::question(this, tr("Copy World"), tr("Changing a world while Minecraft is running is potentially unsafe.\nDo you wish to proceed?"));
+ auto result = QMessageBox::question(this, actionType, tr("Changing a world while Minecraft is running is potentially unsafe.\nDo you wish to proceed?"));
if(result == QMessageBox::No)
{
return false;
@@ -395,7 +395,7 @@ void WorldListPage::on_actionCopy_triggered()
return;
}
- if(!worldSafetyNagQuestion())
+ if(!worldSafetyNagQuestion(tr("Copy World")))
return;
auto worldVariant = m_worlds->data(index, WorldList::ObjectRole);
@@ -417,7 +417,7 @@ void WorldListPage::on_actionRename_triggered()
return;
}
- if(!worldSafetyNagQuestion())
+ if(!worldSafetyNagQuestion(tr("Rename World")))
return;
auto worldVariant = m_worlds->data(index, WorldList::ObjectRole);
diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h
index 17e36a08..1dc9e53e 100644
--- a/launcher/ui/pages/instance/WorldListPage.h
+++ b/launcher/ui/pages/instance/WorldListPage.h
@@ -93,7 +93,7 @@ protected:
private:
QModelIndex getSelectedWorld();
bool isWorldSafe(QModelIndex index);
- bool worldSafetyNagQuestion();
+ bool worldSafetyNagQuestion(const QString &actionType);
void mceditError();
private:
diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp
index 9cf9ec29..029e2be0 100644
--- a/launcher/ui/pages/modplatform/ModModel.cpp
+++ b/launcher/ui/pages/modplatform/ModModel.cpp
@@ -259,10 +259,11 @@ void ListModel::searchRequestFinished(QJsonDocument& doc)
void ListModel::searchRequestFailed(QString reason)
{
- if (!jobPtr->first()->m_reply) {
+ auto failed_action = jobPtr->getFailedActions().at(0);
+ if (!failed_action->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) {
+ } else if (failed_action->m_reply && failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
//: %1 refers to the launcher itself
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp
index 8de5211c..7901b90b 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp
@@ -37,13 +37,12 @@
#include "AtlPage.h"
#include "ui_AtlPage.h"
-#include "modplatform/atlauncher/ATLPackInstallTask.h"
+#include "BuildConfig.h"
#include "AtlOptionalModDialog.h"
+#include "AtlUserInteractionSupportImpl.h"
+#include "modplatform/atlauncher/ATLPackInstallTask.h"
#include "ui/dialogs/NewInstanceDialog.h"
-#include "ui/dialogs/VersionSelectDialog.h"
-
-#include <BuildConfig.h>
#include <QMessageBox>
@@ -117,7 +116,9 @@ void AtlPage::suggestCurrent()
return;
}
- dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.name, selectedVersion));
+ auto uiSupport = new AtlUserInteractionSupportImpl(this);
+ dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(uiSupport, 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)
@@ -172,51 +173,3 @@ void AtlPage::onVersionSelectionChanged(QString data)
selectedVersion = data;
suggestCurrent();
}
-
-QVector<QString> AtlPage::chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods)
-{
- AtlOptionalModDialog optionalModDialog(this, version, mods);
- optionalModDialog.exec();
- return optionalModDialog.getResult();
-}
-
-QString AtlPage::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) {
- VersionSelectDialog vselect(vlist.get(), "Choose Version", APPLICATION->activeWindow(), false);
- if (minecraftVersion != Q_NULLPTR) {
- vselect.setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion);
- vselect.setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion));
- }
- else {
- vselect.setEmptyString(tr("No versions are currently available"));
- }
- vselect.setEmptyErrorString(tr("Couldn't load or download the version lists!"));
-
- // select recommended build
- for (int i = 0; i < vlist->versions().size(); i++) {
- auto version = vlist->versions().at(i);
- auto reqs = version->requires();
-
- // filter by minecraft version, if the loader depends on a certain version.
- if (minecraftVersion != Q_NULLPTR) {
- auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require &req) {
- return req.uid == "net.minecraft";
- });
- if (iter == reqs.end()) continue;
- if (iter->equalsVersion != minecraftVersion) continue;
- }
-
- // first recommended build we find, we use.
- if (version->isRecommended()) {
- vselect.setCurrentVersion(version->descriptor());
- break;
- }
- }
-
- 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 aa6d5da1..1b3b15c1 100644
--- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h
@@ -52,7 +52,7 @@ namespace Ui
class NewInstanceDialog;
-class AtlPage : public QWidget, public BasePage, public ATLauncher::UserInteractionSupport
+class AtlPage : public QWidget, public BasePage
{
Q_OBJECT
@@ -83,10 +83,6 @@ public:
private:
void suggestCurrent();
- QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override;
- QVector<QString> chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) override;
- void displayMessage(QString message) override;
-
private slots:
void triggerSearch();
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
new file mode 100644
index 00000000..03196685
--- /dev/null
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * This program is free software: you can 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 <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 <QMessageBox>
+#include "AtlUserInteractionSupportImpl.h"
+
+#include "AtlOptionalModDialog.h"
+#include "ui/dialogs/VersionSelectDialog.h"
+
+AtlUserInteractionSupportImpl::AtlUserInteractionSupportImpl(QWidget *parent) : m_parent(parent)
+{
+}
+
+QVector<QString> AtlUserInteractionSupportImpl::chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods)
+{
+ AtlOptionalModDialog optionalModDialog(m_parent, version, mods);
+ optionalModDialog.exec();
+ return optionalModDialog.getResult();
+}
+
+QString AtlUserInteractionSupportImpl::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion)
+{
+ VersionSelectDialog vselect(vlist.get(), "Choose Version", m_parent, false);
+ if (minecraftVersion != nullptr) {
+ vselect.setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion);
+ vselect.setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion));
+ }
+ else {
+ vselect.setEmptyString(tr("No versions are currently available"));
+ }
+ vselect.setEmptyErrorString(tr("Couldn't load or download the version lists!"));
+
+ // select recommended build
+ for (int i = 0; i < vlist->versions().size(); i++) {
+ auto version = vlist->versions().at(i);
+ auto reqs = version->requires();
+
+ // filter by minecraft version, if the loader depends on a certain version.
+ if (minecraftVersion != nullptr) {
+ auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require& req) {
+ return req.uid == "net.minecraft";
+ });
+ if (iter == reqs.end())
+ continue;
+ if (iter->equalsVersion != minecraftVersion)
+ continue;
+ }
+
+ // first recommended build we find, we use.
+ if (version->isRecommended()) {
+ vselect.setCurrentVersion(version->descriptor());
+ break;
+ }
+ }
+
+ vselect.exec();
+ return vselect.selectedVersion()->descriptor();
+}
+
+void AtlUserInteractionSupportImpl::displayMessage(QString message)
+{
+ QMessageBox::information(m_parent, tr("Installing"), message);
+}
diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h
new file mode 100644
index 00000000..aa22fc73
--- /dev/null
+++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * This program is free software: you can 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 <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 <QObject>
+
+#include "modplatform/atlauncher/ATLPackInstallTask.h"
+
+class AtlUserInteractionSupportImpl : public QObject, public ATLauncher::UserInteractionSupport {
+ Q_OBJECT
+
+public:
+ AtlUserInteractionSupportImpl(QWidget* parent);
+
+private:
+ QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override;
+ QVector<QString> chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) override;
+ void displayMessage(QString message) override;
+
+private:
+ QWidget* m_parent;
+
+};
diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui
index f4231d8d..ad08dc25 100644
--- a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui
+++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui
@@ -35,7 +35,11 @@
</widget>
</item>
<item row="0" column="1">
- <widget class="QTextBrowser" name="publicPackDescription"/>
+ <widget class="QTextBrowser" name="publicPackDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
</item>
</layout>
</widget>
@@ -45,7 +49,11 @@
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="1">
- <widget class="QTextBrowser" name="thirdPartyPackDescription"/>
+ <widget class="QTextBrowser" name="thirdPartyPackDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
</item>
<item row="0" column="0">
<widget class="QTreeView" name="thirdPartyPackList">
@@ -95,7 +103,11 @@
</widget>
</item>
<item row="0" column="1" rowspan="3">
- <widget class="QTextBrowser" name="privatePackDescription"/>
+ <widget class="QTextBrowser" name="privatePackDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
</item>
</layout>
</widget>
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
index 3633d575..614be434 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
@@ -301,10 +301,11 @@ void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all)
void ModpackListModel::searchRequestFailed(QString reason)
{
- if (!jobPtr->first()->m_reply) {
+ auto failed_action = jobPtr->getFailedActions().at(0);
+ if (!failed_action->m_reply) {
// Network error
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) {
+ } else if (failed_action->m_reply && failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
//: %1 refers to the launcher itself
diff --git a/launcher/ui/widgets/MCModInfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp
index 7d78006b..821e61a7 100644
--- a/launcher/ui/widgets/MCModInfoFrame.cpp
+++ b/launcher/ui/widgets/InfoFrame.cpp
@@ -14,16 +14,30 @@
*/
#include <QMessageBox>
-#include <QtGui>
-#include "MCModInfoFrame.h"
-#include "ui_MCModInfoFrame.h"
+#include "InfoFrame.h"
+#include "ui_InfoFrame.h"
#include "ui/dialogs/CustomMessageBox.h"
-void MCModInfoFrame::updateWithMod(Mod &m)
+InfoFrame::InfoFrame(QWidget *parent) :
+ QFrame(parent),
+ ui(new Ui::InfoFrame)
{
- if (m.type() == m.MOD_FOLDER)
+ ui->setupUi(this);
+ ui->descriptionLabel->setHidden(true);
+ ui->nameLabel->setHidden(true);
+ updateHiddenState();
+}
+
+InfoFrame::~InfoFrame()
+{
+ delete ui;
+}
+
+void InfoFrame::updateWithMod(Mod const& m)
+{
+ if (m.type() == ResourceType::FOLDER)
{
clear();
return;
@@ -43,42 +57,32 @@ void MCModInfoFrame::updateWithMod(Mod &m)
if (!m.authors().isEmpty())
text += " by " + m.authors().join(", ");
- setModText(text);
+ setName(text);
if (m.description().isEmpty())
{
- setModDescription(QString());
+ setDescription(QString());
}
else
{
- setModDescription(m.description());
+ setDescription(m.description());
}
}
-void MCModInfoFrame::clear()
-{
- setModText(QString());
- setModDescription(QString());
-}
-
-MCModInfoFrame::MCModInfoFrame(QWidget *parent) :
- QFrame(parent),
- ui(new Ui::MCModInfoFrame)
+void InfoFrame::updateWithResource(const Resource& resource)
{
- ui->setupUi(this);
- ui->label_ModDescription->setHidden(true);
- ui->label_ModText->setHidden(true);
- updateHiddenState();
+ setName(resource.name());
}
-MCModInfoFrame::~MCModInfoFrame()
+void InfoFrame::clear()
{
- delete ui;
+ setName();
+ setDescription();
}
-void MCModInfoFrame::updateHiddenState()
+void InfoFrame::updateHiddenState()
{
- if(ui->label_ModDescription->isHidden() && ui->label_ModText->isHidden())
+ if(ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden())
{
setHidden(true);
}
@@ -88,34 +92,34 @@ void MCModInfoFrame::updateHiddenState()
}
}
-void MCModInfoFrame::setModText(QString text)
+void InfoFrame::setName(QString text)
{
if(text.isEmpty())
{
- ui->label_ModText->setHidden(true);
+ ui->nameLabel->setHidden(true);
}
else
{
- ui->label_ModText->setText(text);
- ui->label_ModText->setHidden(false);
+ ui->nameLabel->setText(text);
+ ui->nameLabel->setHidden(false);
}
updateHiddenState();
}
-void MCModInfoFrame::setModDescription(QString text)
+void InfoFrame::setDescription(QString text)
{
if(text.isEmpty())
{
- ui->label_ModDescription->setHidden(true);
+ ui->descriptionLabel->setHidden(true);
updateHiddenState();
return;
}
else
{
- ui->label_ModDescription->setHidden(false);
+ ui->descriptionLabel->setHidden(false);
updateHiddenState();
}
- ui->label_ModDescription->setToolTip("");
+ ui->descriptionLabel->setToolTip("");
QString intermediatetext = text.trimmed();
bool prev(false);
QChar rem('\n');
@@ -133,36 +137,36 @@ void MCModInfoFrame::setModDescription(QString text)
labeltext.reserve(300);
if(finaltext.length() > 290)
{
- ui->label_ModDescription->setOpenExternalLinks(false);
- ui->label_ModDescription->setTextFormat(Qt::TextFormat::RichText);
- desc = text;
+ ui->descriptionLabel->setOpenExternalLinks(false);
+ ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText);
+ m_description = text;
// This allows injecting HTML here.
labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
- QObject::connect(ui->label_ModDescription, &QLabel::linkActivated, this, &MCModInfoFrame::modDescEllipsisHandler);
+ QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler);
}
else
{
- ui->label_ModDescription->setTextFormat(Qt::TextFormat::PlainText);
+ ui->descriptionLabel->setTextFormat(Qt::TextFormat::PlainText);
labeltext.append(finaltext);
}
- ui->label_ModDescription->setText(labeltext);
+ ui->descriptionLabel->setText(labeltext);
}
-void MCModInfoFrame::modDescEllipsisHandler(const QString &link)
+void InfoFrame::descriptionEllipsisHandler(QString link)
{
- if(!currentBox)
+ if(!m_current_box)
{
- currentBox = CustomMessageBox::selectable(this, QString(), desc);
- connect(currentBox, &QMessageBox::finished, this, &MCModInfoFrame::boxClosed);
- currentBox->show();
+ m_current_box = CustomMessageBox::selectable(this, "", m_description);
+ connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed);
+ m_current_box->show();
}
else
{
- currentBox->setText(desc);
+ m_current_box->setText(m_description);
}
}
-void MCModInfoFrame::boxClosed(int result)
+void InfoFrame::boxClosed(int result)
{
- currentBox = nullptr;
+ m_current_box = nullptr;
}
diff --git a/launcher/ui/widgets/MCModInfoFrame.h b/launcher/ui/widgets/InfoFrame.h
index 0b7ef537..d69dc232 100644
--- a/launcher/ui/widgets/MCModInfoFrame.h
+++ b/launcher/ui/widgets/InfoFrame.h
@@ -16,37 +16,39 @@
#pragma once
#include <QFrame>
+
#include "minecraft/mod/Mod.h"
+#include "minecraft/mod/ResourcePack.h"
namespace Ui
{
-class MCModInfoFrame;
+class InfoFrame;
}
-class MCModInfoFrame : public QFrame
-{
+class InfoFrame : public QFrame {
Q_OBJECT
-public:
- explicit MCModInfoFrame(QWidget *parent = 0);
- ~MCModInfoFrame();
+ public:
+ InfoFrame(QWidget* parent = nullptr);
+ ~InfoFrame() override;
- void setModText(QString text);
- void setModDescription(QString text);
+ void setName(QString text = {});
+ void setDescription(QString text = {});
- void updateWithMod(Mod &m);
void clear();
-public slots:
- void modDescEllipsisHandler(const QString& link );
+ void updateWithMod(Mod const& m);
+ void updateWithResource(Resource const& resource);
+
+ public slots:
+ void descriptionEllipsisHandler(QString link);
void boxClosed(int result);
-private:
+ private:
void updateHiddenState();
-private:
- Ui::MCModInfoFrame *ui;
- QString desc;
- class QMessageBox * currentBox = nullptr;
+ private:
+ Ui::InfoFrame* ui;
+ QString m_description;
+ class QMessageBox* m_current_box = nullptr;
};
-
diff --git a/launcher/ui/widgets/MCModInfoFrame.ui b/launcher/ui/widgets/InfoFrame.ui
index 5ef33379..0d3772d7 100644
--- a/launcher/ui/widgets/MCModInfoFrame.ui
+++ b/launcher/ui/widgets/InfoFrame.ui
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
- <class>MCModInfoFrame</class>
- <widget class="QFrame" name="MCModInfoFrame">
+ <class>InfoFrame</class>
+ <widget class="QFrame" name="InfoFrame">
<property name="geometry">
<rect>
<x>0</x>
@@ -39,7 +39,7 @@
<number>0</number>
</property>
<item>
- <widget class="QLabel" name="label_ModText">
+ <widget class="QLabel" name="nameLabel">
<property name="text">
<string notr="true"/>
</property>
@@ -61,7 +61,7 @@
</widget>
</item>
<item>
- <widget class="QLabel" name="label_ModDescription">
+ <widget class="QLabel" name="descriptionLabel">
<property name="toolTip">
<string notr="true"/>
</property>
diff --git a/launcher/updater/UpdateChecker.cpp b/launcher/updater/UpdateChecker.cpp
index fa6e5a97..78d979ff 100644
--- a/launcher/updater/UpdateChecker.cpp
+++ b/launcher/updater/UpdateChecker.cpp
@@ -25,12 +25,11 @@
#include "BuildConfig.h"
-UpdateChecker::UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel, int currentBuild)
+UpdateChecker::UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel)
{
m_network = nam;
m_channelUrl = channelUrl;
m_currentChannel = currentChannel;
- m_currentBuild = currentBuild;
#ifdef Q_OS_MAC
m_externalUpdater = new MacSparkleUpdater();
diff --git a/launcher/updater/UpdateChecker.h b/launcher/updater/UpdateChecker.h
index 94e4312b..42ef318b 100644
--- a/launcher/updater/UpdateChecker.h
+++ b/launcher/updater/UpdateChecker.h
@@ -28,7 +28,7 @@ class UpdateChecker : public QObject
Q_OBJECT
public:
- UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel, int currentBuild);
+ UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel);
void checkForUpdate(const QString& updateChannel, bool notifyNoUpdate);
/*!
diff --git a/libraries/README.md b/libraries/README.md
index 37f53385..8e4bd61b 100644
--- a/libraries/README.md
+++ b/libraries/README.md
@@ -3,6 +3,7 @@
This folder has third-party or otherwise external libraries needed for other parts to work.
## classparser
+
A simplistic parser for Java class files.
This library has served as a base for some (much more full-featured and advanced) work under NDA for AVG. It, however, should NOT be confused with that work.
@@ -15,24 +16,22 @@ A performance optimization daemon.
See [github repo](https://github.com/FeralInteractive/gamemode).
-BSD licensed
+BSD-3-Clause licensed
## hoedown
+
Hoedown is a revived fork of Sundown, the Markdown parser based on the original code of the Upskirt library by Natacha Porté.
See [github repo](https://github.com/hoedown/hoedown).
-## iconfix
-This was originally part of the razor-qt project and the Qt toolkit, respecitvely. Its sole purpose is to reimplement Qt's icon loading logic to prevent it from using any platform plugins that could break icon loading.
-
-Licensed under LGPL 2.1
-
## javacheck
+
Simple Java tool that prints the JVM details - version and platform bitness.
Do what you want with it. It is so trivial that noone cares.
## Katabasis
+
Oauth2 library customized for Microsoft authentication.
This is a fork of the [O2 library](https://github.com/pipacs/o2).
@@ -40,9 +39,11 @@ This is a fork of the [O2 library](https://github.com/pipacs/o2).
MIT licensed.
## launcher
+
Java launcher part for Minecraft.
It:
+
* Starts a process
* Waits for a launch script on stdin
* Consumes the launch script you feed it
@@ -56,6 +57,7 @@ A `legacy` and `onesix` launchers are available.
* `onesix` can handle launching any Minecraft version, at the cost of some extra features `legacy` enables (custom window icon and title).
Example (some parts have been censored):
+
```
mod legacyjavafixer-1.0
mainClass net.minecraft.launchwrapper.Launch
@@ -136,6 +138,7 @@ launcher onesix
Available under `GPL-3.0-only` (with classpath exception), sublicensed from its original `Apache-2.0` codebase
## libnbtplusplus
+
libnbt++ is a free C++ library for Minecraft's file format Named Binary Tag (NBT). It can read and write compressed and uncompressed NBT files and provides a code interface for working with NBT data.
See [github repo](https://github.com/ljfa-ag/libnbtplusplus).
@@ -143,6 +146,7 @@ See [github repo](https://github.com/ljfa-ag/libnbtplusplus).
Available either under LGPL version 3 or later.
## LocalPeer
+
Library for making only one instance of the application run at all times.
BSD licensed, derived from [QtSingleApplication](https://github.com/qtproject/qt-solutions/tree/master/qtsingleapplication).
@@ -157,18 +161,19 @@ Public domain (the author disclaimed the copyright).
## quazip
-A zip manipulation library, forked for MultiMC's use.
+A zip manipulation library.
-LGPL 2.1
+LGPL 2.1 with linking exception.
## rainbow
+
Color functions extracted from [KGuiAddons](https://inqlude.org/libraries/kguiaddons.html). Used for adaptive text coloring.
Available either under LGPL version 2.1 or later.
## systeminfo
-A MultiMC-specific library for probing system information.
+A PolyMC-specific library for probing system information.
Apache 2.0
@@ -182,7 +187,6 @@ Licenced under the MIT licence.
## xz-embedded
-Tiny implementation of LZMA2 de/compression. This format is only used by Forge to save bandwidth.
+Tiny implementation of LZMA2 de/compression. This format was only used by Forge to save bandwidth.
Public domain.
-
diff --git a/libraries/katabasis/README.md b/libraries/katabasis/README.md
index 08f3c9d1..621446e1 100644
--- a/libraries/katabasis/README.md
+++ b/libraries/katabasis/README.md
@@ -8,9 +8,9 @@ It may be possible to backport some of the changes to O2 in the future, but for
Notes to contributors:
- * Please follow the coding style of the existing source, where reasonable
- * Code contributions are released under Simplified BSD License, as specified in LICENSE. Do not contribute if this license does not suit your code
- * If you are interested in working on this, come to the PolyMC Discord server and talk first
+* Please follow the coding style of the existing source, where reasonable
+* Code contributions are released under Simplified BSD License, as specified in LICENSE. Do not contribute if this license does not suit your code
+* If you are interested in working on this, come to the PolyMC Discord server and talk first
## Installation
diff --git a/libraries/katabasis/acknowledgements.md b/libraries/katabasis/acknowledgements.md
index c1c8a3d4..ccc7c263 100644
--- a/libraries/katabasis/acknowledgements.md
+++ b/libraries/katabasis/acknowledgements.md
@@ -1,4 +1,4 @@
-# O2 library by Akos Polster and contributors
+## O2 library by Akos Polster and contributors
[The origin of this fork.](https://github.com/pipacs/o2)
@@ -26,17 +26,16 @@
> OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
> OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-# SimpleCrypt by Andre Somers
+## SimpleCrypt by Andre Somers
Cryptographic methods for Qt.
> Copyright (c) 2011, Andre Somers
> All rights reserved.
->
+>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions are met:
->
+>
> * Redistributions of source code must retain the above copyright
> notice, this list of conditions and the following disclaimer.
> * Redistributions in binary form must reproduce the above copyright
@@ -45,7 +44,7 @@ Cryptographic methods for Qt.
> * Neither the name of the Rathenau Instituut, Andre Somers nor the
> names of its contributors may be used to endorse or promote products
> derived from this software without specific prior written permission.
->
+>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
> ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
@@ -57,54 +56,53 @@ Cryptographic methods for Qt.
> (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-# Mandeep Sandhu <mandeepsandhu.chd@gmail.com>
+## Mandeep Sandhu <mandeepsandhu.chd@gmail.com>
Configurable settings storage, Twitter XAuth specialization, new demos, cleanups.
> "Hi Akos,
->
+>
> I'm writing this mail to confirm that my contributions to the O2 library, available here https://github.com/pipacs/o2, can be freely distributed according to the project's license (as shown in the LICENSE file).
->
+>
> Regards,
> -mandeep"
-# Sergey Gavrushkin <https://github.com/ncux>
+## Sergey Gavrushkin <https://github.com/ncux>
FreshBooks specialization
-# Theofilos Intzoglou <https://github.com/parapente>
+## Theofilos Intzoglou <https://github.com/parapente>
Hubic specialization
-# Dimitar
+## Dimitar
SurveyMonkey specialization
-# David Brooks <https://github.com/dbrnz>
+## David Brooks <https://github.com/dbrnz>
CMake related fixes and improvements.
-# Lukas Vogel <https://github.com/lukedirtwalker>
+## Lukas Vogel <https://github.com/lukedirtwalker>
Spotify support
-# Alan Garny <https://github.com/agarny>
+## Alan Garny <https://github.com/agarny>
Windows DLL build support
-# MartinMikita <https://github.com/MartinMikita>
+## MartinMikita <https://github.com/MartinMikita>
Bug fixes
-# Larry Shaffer <https://github.com/dakcarto>
+## Larry Shaffer <https://github.com/dakcarto>
Versioning, shared lib, install target and header support
-# Gilmanov Ildar <https://github.com/gilmanov-ildar>
+## Gilmanov Ildar <https://github.com/gilmanov-ildar>
Bug fixes, support for ```qml``` module
-# Fabian Vogt <https://github.com/Vogtinator>
+## Fabian Vogt <https://github.com/Vogtinator>
Bug fixes, support for building without Qt keywords enabled
-
diff --git a/libraries/murmur2/src/MurmurHash2.cpp b/libraries/murmur2/src/MurmurHash2.cpp
index 3e52e6d1..b625efb1 100644
--- a/libraries/murmur2/src/MurmurHash2.cpp
+++ b/libraries/murmur2/src/MurmurHash2.cpp
@@ -1,86 +1,110 @@
//-----------------------------------------------------------------------------
// MurmurHash2 was written by Austin Appleby, and is placed in the public
// domain. The author hereby disclaims copyright to this source code.
-
-// Note - This code makes a few assumptions about how your machine behaves -
-
-// 1. We can read a 4-byte value from any address without crashing
-// 2. sizeof(int) == 4
-
-// And it has a few limitations -
-
-// 1. It will not work incrementally.
-// 2. It will not produce the same results on little-endian and big-endian
-// machines.
+//
+// This was modified as to possibilitate it's usage incrementally.
+// Those modifications are also placed in the public domain, and the author of
+// such modifications hereby disclaims copyright to this source code.
#include "MurmurHash2.h"
//-----------------------------------------------------------------------------
-// Platform-specific functions and macros
-
-// Microsoft Visual Studio
-
-#if defined(_MSC_VER)
-
-#define BIG_CONSTANT(x) (x)
-// Other compilers
+// 'm' and 'r' are mixing constants generated offline.
+// They're not really 'magic', they just happen to work well.
+const uint32_t m = 0x5bd1e995;
+const int r = 24;
-#else // defined(_MSC_VER)
-
-#define BIG_CONSTANT(x) (x##LLU)
-
-#endif // !defined(_MSC_VER)
-
-//-----------------------------------------------------------------------------
-
-uint64_t MurmurHash2 ( const void* key, int len, uint32_t seed )
+uint32_t MurmurHash2(std::ifstream&& file_stream, std::size_t buffer_size, std::function<bool(char)> filter_out)
{
- // 'm' and 'r' are mixing constants generated offline.
- // They're not really 'magic', they just happen to work well.
-
- const uint32_t m = 0x5bd1e995;
- const int r = 24;
-
- // Initialize the hash to a 'random' value
-
- uint32_t h = seed ^ len;
-
- // Mix 4 bytes at a time into the hash
- const auto* data = (const unsigned char*) key;
- while(len >= 4)
- {
- uint32_t k = *(uint32_t*)data;
-
- k *= m;
- k ^= k >> r;
- k *= m;
-
- h *= m;
- h ^= k;
-
- data += 4*sizeof(char);
- len -= 4;
- }
-
- // Handle the last few bytes of the input array
-
- switch(len)
- {
- case 3: h ^= data[2] << 16;
- case 2: h ^= data[1] << 8;
- case 1: h ^= data[0];
- h *= m;
- };
-
- // Do a few final mixes of the hash to ensure the last few
- // bytes are well-incorporated.
-
- h ^= h >> 13;
- h *= m;
- h ^= h >> 15;
-
- return h;
-}
+ auto* buffer = new char[buffer_size];
+ char data[4];
+
+ int read = 0;
+ uint32_t size = 0;
+
+ // We need the size without the filtered out characters before actually calculating the hash,
+ // to setup the initial value for the hash.
+ do {
+ file_stream.read(buffer, buffer_size);
+ read = file_stream.gcount();
+ for (int i = 0; i < read; i++) {
+ if (!filter_out(buffer[i]))
+ size += 1;
+ }
+ } while (!file_stream.eof());
+
+ file_stream.clear();
+ file_stream.seekg(0, file_stream.beg);
+
+ int index = 0;
+
+ // This forces a seed of 1.
+ IncrementalHashInfo info{ (uint32_t)1 ^ size, (uint32_t)size };
+ do {
+ file_stream.read(buffer, buffer_size);
+ read = file_stream.gcount();
+ for (int i = 0; i < read; i++) {
+ char c = buffer[i];
+
+ if (filter_out(c))
+ continue;
+
+ data[index] = c;
+ index = (index + 1) % 4;
+
+ // Mix 4 bytes at a time into the hash
+ if (index == 0)
+ FourBytes_MurmurHash2((unsigned char*)&data, info);
+ }
+ } while (!file_stream.eof());
+
+ // Do one last bit shuffle in the hash
+ FourBytes_MurmurHash2((unsigned char*)&data, info);
+
+ delete[] buffer;
+
+ file_stream.close();
+ return info.h;
+}
+
+void FourBytes_MurmurHash2(const unsigned char* data, IncrementalHashInfo& prev)
+{
+ if (prev.len >= 4) {
+ // Not the final mix
+ uint32_t k = *(uint32_t*)data;
+
+ k *= m;
+ k ^= k >> r;
+ k *= m;
+
+ prev.h *= m;
+ prev.h ^= k;
+
+ prev.len -= 4;
+ } else {
+ // The final mix
+
+ // Handle the last few bytes of the input array
+ switch (prev.len) {
+ case 3:
+ prev.h ^= data[2] << 16;
+ case 2:
+ prev.h ^= data[1] << 8;
+ case 1:
+ prev.h ^= data[0];
+ prev.h *= m;
+ };
+
+ // Do a few final mixes of the hash to ensure the last few
+ // bytes are well-incorporated.
+
+ prev.h ^= prev.h >> 13;
+ prev.h *= m;
+ prev.h ^= prev.h >> 15;
+
+ prev.len = 0;
+ }
+}
//-----------------------------------------------------------------------------
diff --git a/libraries/murmur2/src/MurmurHash2.h b/libraries/murmur2/src/MurmurHash2.h
index c7b83bca..dc2c9681 100644
--- a/libraries/murmur2/src/MurmurHash2.h
+++ b/libraries/murmur2/src/MurmurHash2.h
@@ -1,30 +1,33 @@
//-----------------------------------------------------------------------------
-// MurmurHash2 was written by Austin Appleby, and is placed in the public
-// domain. The author hereby disclaims copyright to this source code.
+// The original MurmurHash2 was written by Austin Appleby, and is placed in the
+// public domain. The author hereby disclaims copyright to this source code.
+//
+// This was modified as to possibilitate it's usage incrementally.
+// Those modifications are also placed in the public domain, and the author of
+// such modifications hereby disclaims copyright to this source code.
#pragma once
-//-----------------------------------------------------------------------------
-// Platform-specific functions and macros
-
-// Microsoft Visual Studio
-
-#if defined(_MSC_VER) && (_MSC_VER < 1600)
+#include <cstdint>
+#include <fstream>
-typedef unsigned char uint8_t;
-typedef unsigned int uint32_t;
-typedef unsigned __int64 uint64_t;
+#include <functional>
-// Other compilers
-
-#else // defined(_MSC_VER)
+//-----------------------------------------------------------------------------
-#include <stdint.h>
+#define KiB 1024
+#define MiB 1024*KiB
-#endif // !defined(_MSC_VER)
+uint32_t MurmurHash2(
+ std::ifstream&& file_stream,
+ std::size_t buffer_size = 4*MiB,
+ std::function<bool(char)> filter_out = [](char) { return false; });
-//-----------------------------------------------------------------------------
+struct IncrementalHashInfo {
+ uint32_t h;
+ uint32_t len;
+};
-uint64_t MurmurHash2 ( const void* key, int len, uint32_t seed = 1 );
+void FourBytes_MurmurHash2(const unsigned char* data, IncrementalHashInfo& prev);
//-----------------------------------------------------------------------------
diff --git a/libraries/tomlc99/README.md b/libraries/tomlc99/README.md
index 6715b5be..e5fe9480 100644
--- a/libraries/tomlc99/README.md
+++ b/libraries/tomlc99/README.md
@@ -10,7 +10,6 @@ If you are looking for a C++ library, you might try this wrapper: [https://githu
[iarna/toml-spec-tests](https://github.com/iarna/toml-spec-tests).
* Provides very simple and intuitive interface.
-
## Usage
Please see the `toml.h` file for details. What follows is a simple example that
@@ -18,8 +17,8 @@ parses this config file:
```toml
[server]
- host = "www.example.com"
- port = [ 8080, 8181, 8282 ]
+ host = "www.example.com"
+ port = [ 8080, 8181, 8282 ]
```
The steps for getting values from our file is usually :
@@ -96,13 +95,14 @@ int main()
}
```
-#### Accessing Table Content
+### Accessing Table Content
TOML tables are dictionaries where lookups are done using string keys. In
general, all access functions on tables are named `toml_*_in(...)`.
In the normal case, you know the key and its content type, and retrievals can be done
using one of these functions:
+
```c
toml_string_in(tab, key);
toml_bool_in(tab, key);
@@ -114,6 +114,7 @@ toml_array_in(tab, key);
```
You can also interrogate the keys in a table using an integer index:
+
```c
toml_table_t* tab = toml_parse_file(...);
for (int i = 0; ; i++) {
@@ -123,16 +124,18 @@ for (int i = 0; ; i++) {
}
```
-#### Accessing Array Content
+### Accessing Array Content
TOML arrays can be deref-ed using integer indices. In general, all access methods on arrays are named `toml_*_at()`.
To obtain the size of an array:
+
```c
int size = toml_array_nelem(arr);
```
To obtain the content of an array, use a valid index and call one of these functions:
+
```c
toml_string_at(arr, idx);
toml_bool_at(arr, idx);
@@ -143,7 +146,7 @@ toml_table_at(arr, idx);
toml_array_at(arr, idx);
```
-#### toml_datum_t
+### toml_datum_t
Some `toml_*_at` and `toml_*_in` functions return a toml_datum_t
structure. The `ok` flag in the structure indicates if the function
@@ -151,15 +154,16 @@ call was successful. If so, you may proceed to read the value
corresponding to the type of the content.
For example:
-```
+
+```c
toml_datum_t host = toml_string_in(tab, "host");
if (host.ok) {
- printf("host: %s\n", host.u.s);
- free(host.u.s); /* FREE applies to string and timestamp types only */
+ printf("host: %s\n", host.u.s);
+ free(host.u.s); /* FREE applies to string and timestamp types only */
}
```
-** IMPORTANT: if the accessed value is a string or a timestamp, you must call `free(datum.u.s)` or `free(datum.u.ts)` respectively after usage. **
+**IMPORTANT: if the accessed value is a string or a timestamp, you must call `free(datum.u.s)` or `free(datum.u.ts)` respectively after usage.**
## Building and installing
@@ -183,7 +187,6 @@ To test against the standard test set provided by BurntSushi/toml-test:
% bash run.sh # this will run the test suite
```
-
To test against the standard test set provided by iarna/toml:
```sh
diff --git a/nix/NIX.md b/nix/NIX.md
index 1ceba9a3..047dd82f 100644
--- a/nix/NIX.md
+++ b/nix/NIX.md
@@ -1,20 +1,23 @@
# How to import
To import with flakes use
+
```nix
-inputs = {
- polymc.url = "github:PolyMC/PolyMC";
-};
+{
+ inputs = {
+ polymc.url = "github:PolyMC/PolyMC";
+ };
...
-nixpkgs.overlays = [ inputs.polymc.overlay ]; ## Within configuration.nix
-environment.systemPackages = with pkgs; [ polymc ]; ##
+ nixpkgs.overlays = [ inputs.polymc.overlay ]; ## Within configuration.nix
+ environment.systemPackages = with pkgs; [ polymc ]; ##
+}
```
To import without flakes use channels:
-```
+```sh
nix-channel --add https://github.com/PolyMC/PolyMC/archive/master.tar.gz polymc
nix-channel --update polymc
nix-env -iA polymc
@@ -22,10 +25,12 @@ nix-env -iA polymc
or alternatively you can use
-```
-nixpkgs.overlays = [
- (import (builtins.fetchTarball "https://github.com/PolyMC/PolyMC/archive/develop.tar.gz")).overlay
-];
+```nix
+{
+ nixpkgs.overlays = [
+ (import (builtins.fetchTarball "https://github.com/PolyMC/PolyMC/archive/develop.tar.gz")).overlay
+ ];
-environment.systemPackages = with pkgs; [ polymc ];
+ environment.systemPackages = with pkgs; [ polymc ];
+}
```
diff --git a/program_info/CMakeLists.txt b/program_info/CMakeLists.txt
index b1ba89df..ac8ea6ce 100644
--- a/program_info/CMakeLists.txt
+++ b/program_info/CMakeLists.txt
@@ -15,7 +15,7 @@ set(Launcher_Copyright "${Launcher_Copyright}" PARENT_SCOPE)
set(Launcher_Domain "polymc.org" PARENT_SCOPE)
set(Launcher_Name "${Launcher_CommonName}" PARENT_SCOPE)
set(Launcher_DisplayName "${Launcher_CommonName}" PARENT_SCOPE)
-set(Launcher_UserAgent "${Launcher_CommonName}/${Launcher_RELEASE_VERSION_NAME}" PARENT_SCOPE)
+set(Launcher_UserAgent "${Launcher_CommonName}/${Launcher_VERSION_NAME}" PARENT_SCOPE)
set(Launcher_ConfigFile "polymc.cfg" PARENT_SCOPE)
set(Launcher_Git "https://github.com/PolyMC/PolyMC" PARENT_SCOPE)
set(Launcher_DesktopFileName "org.polymc.PolyMC.desktop" PARENT_SCOPE)
diff --git a/program_info/README.md b/program_info/README.md
index 1e805d4a..421ef1f9 100644
--- a/program_info/README.md
+++ b/program_info/README.md
@@ -1,6 +1,7 @@
# PolyMC Program Info
This is PolyMC's program info which contains information about:
+
- Application name and logo (and branding in general)
- Various URLs and API endpoints
- Desktop file
diff --git a/program_info/org.polymc.PolyMC.metainfo.xml.in b/program_info/org.polymc.PolyMC.metainfo.xml.in
index ea665655..db0ab882 100644
--- a/program_info/org.polymc.PolyMC.metainfo.xml.in
+++ b/program_info/org.polymc.PolyMC.metainfo.xml.in
@@ -6,7 +6,7 @@
</provides>
<launchable type="desktop-id">org.polymc.PolyMC.desktop</launchable>
<name>PolyMC</name>
- <developer_name>PolyMC Team</developer_name>
+ <developer_name>PolyMC</developer_name>
<summary>A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once</summary>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-only</project_license>
@@ -16,39 +16,43 @@
<p>PolyMC is a custom launcher for Minecraft that focuses on predictability, long term stability and simplicity.</p>
<p>Features:</p>
<ul>
- <li>Easily install game modifications, such as Fabric or Forge</li>
+ <li>Easily install game modifications, such as Fabric, Forge and Quilt</li>
<li>Control your java settings</li>
<li>Manage worlds and resource packs from the launcher</li>
<li>See logs and other details easily</li>
<li>Kill Minecraft in case of a crash/freeze</li>
<li>Isolate minecraft instances to keep everything clean</li>
- <li>Install mods directly from the launcher</li>
+ <li>Install and update mods directly from the launcher</li>
</ul>
</description>
<screenshots>
<screenshot type="default">
<caption>The main PolyMC window</caption>
- <image type="source" width="931" height="759">https://polymc.org/img/screenshots/LauncherDark.png</image>
+ <image type="source" width="578" height="452">https://polymc.org/img/screenshots/LauncherDark.png</image>
</screenshot>
<screenshot>
<caption>Modpack installation</caption>
- <image type="source" width="860" height="848">https://polymc.org/img/screenshots/ModpackInstallDark.png</image>
+ <image type="source" width="523" height="452">https://polymc.org/img/screenshots/ModpackInstallDark.png</image>
</screenshot>
<screenshot>
<caption>Mod installation</caption>
- <image type="source" width="1018" height="858">https://polymc.org/img/screenshots/ModInstallDark.png</image>
+ <image type="source" width="654" height="452">https://polymc.org/img/screenshots/ModInstallDark.png</image>
+ </screenshot>
+ <screenshot>
+ <caption>Mod updating</caption>
+ <image type="source" width="490" height="452">https://polymc.org/img/screenshots/ModUpdateDark.png</image>
</screenshot>
<screenshot>
<caption>Instance management</caption>
- <image type="source" width="777" height="693">https://polymc.org/img/screenshots/PropertiesDark.png</image>
+ <image type="source" width="667" height="452">https://polymc.org/img/screenshots/PropertiesDark.png</image>
</screenshot>
<screenshot>
<caption>Cat :)</caption>
- <image type="source" width="931" height="759">https://polymc.org/img/screenshots/LauncherCatDark.png</image>
+ <image type="source" width="555" height="452">https://polymc.org/img/screenshots/LauncherCatDark.png</image>
</screenshot>
</screenshots>
<releases>
- <release version="@Launcher_RELEASE_VERSION_NAME@" date="@Launcher_RELEASE_TIMESTAMP@"></release>
+ <release version="@Launcher_VERSION_NAME@" date="@Launcher_BUILD_TIMESTAMP@"></release>
</releases>
<content_rating type="oars-1.1">
<content_attribute id="violence-fantasy">moderate</content_attribute>
diff --git a/program_info/polymc.manifest.in b/program_info/polymc.manifest.in
index 0eefacac..b85b6d46 100644
--- a/program_info/polymc.manifest.in
+++ b/program_info/polymc.manifest.in
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
- <assemblyIdentity name="PolyMC.Application.1" type="win32" version="@Launcher_RELEASE_VERSION_NAME4@" />
+ <assemblyIdentity name="PolyMC.Application.1" type="win32" version="@Launcher_VERSION_NAME4@" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
diff --git a/program_info/polymc.rc.in b/program_info/polymc.rc.in
index 0ea9b73a..be51ad71 100644
--- a/program_info/polymc.rc.in
+++ 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 @Launcher_RELEASE_VERSION_NAME4_COMMA@
+FILEVERSION @Launcher_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", "@Launcher_RELEASE_VERSION_NAME4@"
+ VALUE "FileVersion", "@Launcher_VERSION_NAME4@"
VALUE "ProductName", "PolyMC"
- VALUE "ProductVersion", "@Launcher_RELEASE_VERSION_NAME4@"
+ VALUE "ProductVersion", "@Launcher_VERSION_NAME4@"
END
END
BLOCK "VarFileInfo"
diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in
index 84c3766e..87e266f8 100644
--- a/program_info/win_install.nsi.in
+++ b/program_info/win_install.nsi.in
@@ -102,13 +102,13 @@ OutFile "../@Launcher_CommonName@-Setup.exe"
;--------------------------------
; Version info
-VIProductVersion "@Launcher_RELEASE_VERSION_NAME4@"
-VIFileVersion "@Launcher_RELEASE_VERSION_NAME4@"
+VIProductVersion "@Launcher_VERSION_NAME4@"
+VIFileVersion "@Launcher_VERSION_NAME4@"
VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductName" "@Launcher_CommonName@"
VIAddVersionKey /LANG=${LANG_ENGLISH} "FileDescription" "@Launcher_CommonName@ Installer"
VIAddVersionKey /LANG=${LANG_ENGLISH} "LegalCopyright" "@Launcher_Copyright@"
-VIAddVersionKey /LANG=${LANG_ENGLISH} "FileVersion" "@Launcher_RELEASE_VERSION_NAME4@"
-VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_RELEASE_VERSION_NAME4@"
+VIAddVersionKey /LANG=${LANG_ENGLISH} "FileVersion" "@Launcher_VERSION_NAME4@"
+VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@"
;--------------------------------
@@ -145,8 +145,8 @@ 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}" "Version" "@Launcher_RELEASE_VERSION_NAME4@"
- WriteRegStr HKCU "${UNINST_KEY}" "DisplayVersion" "@Launcher_RELEASE_VERSION_NAME@"
+ WriteRegStr HKCU "${UNINST_KEY}" "Version" "@Launcher_VERSION_NAME4@"
+ WriteRegStr HKCU "${UNINST_KEY}" "DisplayVersion" "@Launcher_VERSION_NAME@"
WriteRegStr HKCU "${UNINST_KEY}" "VersionMajor" "@Launcher_VERSION_MAJOR@"
WriteRegStr HKCU "${UNINST_KEY}" "VersionMinor" "@Launcher_VERSION_MINOR@"
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2