diff options
239 files changed, 6428 insertions, 3966 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..434c5775 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 @@ -259,10 +249,8 @@ jobs: cmake --install ${{ env.BUILD_DIR }} cd ${{ env.INSTALL_DIR }} - if [ "${{ matrix.msystem }}" == "mingw32" ]; then + if [ "${{ matrix.qt_ver }}" == "5" ]; then cp /mingw32/bin/libcrypto-1_1.dll /mingw32/bin/libssl-1_1.dll ./ - elif [ "${{ matrix.msystem }}" == "mingw64" ]; then - cp /mingw64/bin/libcrypto-1_1-x64.dll /mingw64/bin/libssl-1_1-x64.dll ./ fi - name: Package (Windows, portable) @@ -314,6 +302,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 33c53b82..7100ab1b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,10 +29,10 @@ set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${PROJECT_BINARY_DIR}/jars) ######## Set compiler flags ######## set(CMAKE_CXX_STANDARD_REQUIRED true) set(CMAKE_C_STANDARD_REQUIRED true) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) -set(CMAKE_CXX_FLAGS "-Wall -pedantic -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS "-Wall -pedantic -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}") if(UNIX AND APPLE) set(CMAKE_CXX_FLAGS "-stdlib=libc++ ${CMAKE_CXX_FLAGS}") endif() @@ -67,7 +67,7 @@ find_package(ECM REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH "${ECM_MODULE_PATH};${CMAKE_MODULE_PATH}") include(CTest) include(ECMAddTests) -if (BUILD_TESTING) +if(BUILD_TESTING) enable_testing() endif() @@ -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=") @@ -319,7 +312,6 @@ endif() add_subdirectory(libraries/rainbow) # Qt extension for colors add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions add_subdirectory(libraries/classparser) # class parser library -add_subdirectory(libraries/optional-bare) add_subdirectory(libraries/tomlc99) # toml parser add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much add_subdirectory(libraries/gamemode) @@ -329,5 +321,8 @@ add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API add_subdirectory(buildconfig) +if(BUILD_TESTING) + add_subdirectory(tests) +endif() # NOTE: this must always be last to appease the CMake deity of quirky install command evaluation order. add_subdirectory(launcher) 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 @@ -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,35 +315,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# optional-bare - - Code from https://github.com/martinmoene/optional-bare/ - - Boost Software License - Version 1.0 - August 17th, 2003 - - Permission is hereby granted, free of charge, to any person or organization - obtaining a copy of the software and accompanying documentation covered by - this license (the "Software") to use, reproduce, display, distribute, - execute, and transmit the Software, and to prepare derivative works of the - Software, and to permit third-parties to whom the Software is furnished to - do so, all subject to the following: - - The copyright notices in the Software and this entire statement, including - the above license grant, this restriction and the following disclaimer, - must be included in all copies of the Software, in whole or in part, and - all derivative works of the Software, unless such copies or derivative - works are solely in the form of machine-executable object code generated by - a source language processor. - - 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT - SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE - FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, - ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. - -# tomlc99 +## tomlc99 MIT License @@ -348,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. @@ -373,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. @@ -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 6e25b774..c4179b49 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -117,6 +117,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 @@ -325,7 +330,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) @@ -684,6 +689,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("UpdateDialogGeometry", ""); + m_settings->registerSetting("ModDownloadGeometry", ""); + // HACK: This code feels so stupid is there a less stupid way of doing this? { m_settings->registerSetting("PastebinURL", ""); @@ -778,7 +785,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."; } @@ -868,6 +875,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()); @@ -923,6 +931,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) { return; } + + updateCapabilities(); performMainStartupAction(); } @@ -1271,6 +1281,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; @@ -1584,14 +1597,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 e7afcfc0..e44b98eb 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -88,12 +88,6 @@ set(CORE_SOURCES MMCTime.cpp ) -ecm_add_test(FileSystem_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME FileSystem) # TODO: needs testdata - -ecm_add_test(GZip_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME GZip) - set(PATHMATCHER_SOURCES # Path matchers pathmatcher/FSTreeMatcher.h @@ -318,16 +312,26 @@ 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/ResourcePack.h + minecraft/mod/ResourcePack.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 minecraft/mod/tasks/LocalModParseTask.cpp minecraft/mod/tasks/LocalModUpdateTask.h minecraft/mod/tasks/LocalModUpdateTask.cpp + minecraft/mod/tasks/LocalResourcePackParseTask.h + minecraft/mod/tasks/LocalResourcePackParseTask.cpp # Assets minecraft/AssetsUtils.h @@ -345,42 +349,6 @@ set(MINECRAFT_SOURCES mojang/PackageManifest.cpp minecraft/Agent.h) -ecm_add_test(minecraft/GradleSpecifier_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME GradleSpecifier) - -if(BUILD_TESTING) - add_executable(PackageManifest - mojang/PackageManifest_test.cpp - ) - target_link_libraries(PackageManifest - Launcher_logic - Qt${QT_VERSION_MAJOR}::Test - ) - target_include_directories(PackageManifest - PRIVATE ../cmake/UnitTest/ - ) - add_test( - NAME PackageManifest - COMMAND PackageManifest - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) -endif() - -# TODO: needs minecraft/testdata -ecm_add_test(minecraft/MojangVersionFormat_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME MojangVersionFormat) - -ecm_add_test(minecraft/Library_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME Library) - -# 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/ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME ParseUtils) - # the screenshots feature set(SCREENSHOTS_SOURCES screenshots/Screenshot.h @@ -402,9 +370,6 @@ set(TASKS_SOURCES tasks/MultipleOptionsTask.cpp ) -ecm_add_test(tasks/Task_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME Task) - set(SETTINGS_SOURCES # Settings settings/INIFile.cpp @@ -421,9 +386,6 @@ set(SETTINGS_SOURCES settings/SettingsObject.h ) -ecm_add_test(settings/INIFile_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME INIFile) - set(JAVA_SOURCES java/JavaChecker.h java/JavaChecker.cpp @@ -439,9 +401,6 @@ set(JAVA_SOURCES java/JavaVersion.cpp ) -ecm_add_test(java/JavaVersion_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME JavaVersion) - set(TRANSLATIONS_SOURCES translations/TranslationsModel.h translations/TranslationsModel.cpp @@ -494,6 +453,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 @@ -542,9 +503,6 @@ set(PACKWIZ_SOURCES modplatform/packwiz/Packwiz.cpp ) -# TODO: needs modplatform/packwiz/testdata -ecm_add_test(modplatform/packwiz/Packwiz_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME Packwiz) set(TECHNIC_SOURCES modplatform/technic/SingleZipPackInstallTask.h @@ -568,9 +526,6 @@ set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLShareCode.h ) -ecm_add_test(meta/Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME Index) - ################################ COMPILE ################################ # we need zlib @@ -764,6 +719,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 +806,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 +834,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 @@ -884,6 +843,8 @@ SET(LAUNCHER_SOURCES ui/widgets/PageContainer.cpp ui/widgets/PageContainer.h ui/widgets/PageContainer_p.h + ui/widgets/ProjectItem.h + ui/widgets/ProjectItem.cpp ui/widgets/VersionListView.cpp ui/widgets/VersionListView.h ui/widgets/VersionSelectWidget.cpp @@ -948,7 +909,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 @@ -968,6 +929,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 ) @@ -992,13 +954,13 @@ endif() # Add executable add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) +target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(Launcher_logic systeminfo Launcher_classparser Launcher_murmur2 nbt++ ${ZLIB_LIBRARIES} - optional-bare tomlc99 BuildConfig Katabasis diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index ebb4460d..8eeb2885 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -35,76 +35,64 @@ #include "FileSystem.h" +#include <QDebug> #include <QDir> #include <QFile> -#include <QSaveFile> #include <QFileInfo> -#include <QDebug> -#include <QUrl> +#include <QSaveFile> #include <QStandardPaths> #include <QTextStream> +#include <QUrl> #if defined Q_OS_WIN32 - #include <windows.h> - #include <string> - #include <sys/utime.h> - #include <winnls.h> - #include <shobjidl.h> - #include <objbase.h> - #include <objidl.h> - #include <shlguid.h> - #include <shlobj.h> +#include <objbase.h> +#include <objidl.h> +#include <shlguid.h> +#include <shlobj.h> +#include <shobjidl.h> +#include <sys/utime.h> +#include <windows.h> +#include <winnls.h> +#include <string> #else - #include <utime.h> +#include <utime.h> #endif namespace FS { -void ensureExists(const QDir &dir) +void ensureExists(const QDir& dir) { - if (!QDir().mkpath(dir.absolutePath())) - { - throw FileSystemException("Unable to create folder " + dir.dirName() + " (" + - dir.absolutePath() + ")"); + if (!QDir().mkpath(dir.absolutePath())) { + throw FileSystemException("Unable to create folder " + dir.dirName() + " (" + dir.absolutePath() + ")"); } } -void write(const QString &filename, const QByteArray &data) +void write(const QString& filename, const QByteArray& data) { ensureExists(QFileInfo(filename).dir()); QSaveFile file(filename); - if (!file.open(QSaveFile::WriteOnly)) - { - throw FileSystemException("Couldn't open " + filename + " for writing: " + - file.errorString()); + if (!file.open(QSaveFile::WriteOnly)) { + throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } - if (data.size() != file.write(data)) - { - throw FileSystemException("Error writing data to " + filename + ": " + - file.errorString()); + if (data.size() != file.write(data)) { + throw FileSystemException("Error writing data to " + filename + ": " + file.errorString()); } - if (!file.commit()) - { - throw FileSystemException("Error while committing data to " + filename + ": " + - file.errorString()); + if (!file.commit()) { + throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString()); } } -QByteArray read(const QString &filename) +QByteArray read(const QString& filename) { QFile file(filename); - if (!file.open(QFile::ReadOnly)) - { - throw FileSystemException("Unable to open " + filename + " for reading: " + - file.errorString()); + if (!file.open(QFile::ReadOnly)) { + throw FileSystemException("Unable to open " + filename + " for reading: " + file.errorString()); } const qint64 size = file.size(); QByteArray data(int(size), 0); const qint64 ret = file.read(data.data(), size); - if (ret == -1 || ret != size) - { - throw FileSystemException("Error reading data from " + filename + ": " + - file.errorString()); + if (ret == -1 || ret != size) { + throw FileSystemException("Error reading data from " + filename + ": " + file.errorString()); } return data; } @@ -138,12 +126,12 @@ bool ensureFolderPathExists(QString foldernamepath) return success; } -bool copy::operator()(const QString &offset) +bool copy::operator()(const QString& offset) { - //NOTE always deep copy on windows. the alternatives are too messy. - #if defined Q_OS_WIN32 +// NOTE always deep copy on windows. the alternatives are too messy. +#if defined Q_OS_WIN32 m_followSymlinks = true; - #endif +#endif auto src = PathCombine(m_src.absolutePath(), offset); auto dst = PathCombine(m_dst.absolutePath(), offset); @@ -152,52 +140,39 @@ bool copy::operator()(const QString &offset) if (!currentSrc.exists()) return false; - if(!m_followSymlinks && currentSrc.isSymLink()) - { + if (!m_followSymlinks && currentSrc.isSymLink()) { qDebug() << "creating symlink" << src << " - " << dst; - if (!ensureFilePathExists(dst)) - { + if (!ensureFilePathExists(dst)) { qWarning() << "Cannot create path!"; return false; } return QFile::link(currentSrc.symLinkTarget(), dst); - } - else if(currentSrc.isFile()) - { + } else if (currentSrc.isFile()) { qDebug() << "copying file" << src << " - " << dst; - if (!ensureFilePathExists(dst)) - { + if (!ensureFilePathExists(dst)) { qWarning() << "Cannot create path!"; return false; } return QFile::copy(src, dst); - } - else if(currentSrc.isDir()) - { + } else if (currentSrc.isDir()) { qDebug() << "recursing" << offset; - if (!ensureFolderPathExists(dst)) - { + if (!ensureFolderPathExists(dst)) { qWarning() << "Cannot create path!"; return false; } QDir currentDir(src); - for(auto & f : currentDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) - { + for (auto& f : currentDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) { auto inner_offset = PathCombine(offset, f); // ignore and skip stuff that matches the blacklist. - if(m_blacklist && m_blacklist->matches(inner_offset)) - { + if (m_blacklist && m_blacklist->matches(inner_offset)) { continue; } - if(!operator()(inner_offset)) - { + if (!operator()(inner_offset)) { qWarning() << "Failed to copy" << inner_offset; return false; } } - } - else - { + } else { qCritical() << "Copy ERROR: Unknown filesystem object:" << src; return false; } @@ -208,55 +183,41 @@ bool deletePath(QString path) { bool OK = true; QFileInfo finfo(path); - if(finfo.isFile()) { + if (finfo.isFile()) { return QFile::remove(path); } QDir dir(path); - if (!dir.exists()) - { + if (!dir.exists()) { return OK; } - auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | - QDir::AllDirs | QDir::Files, - QDir::DirsFirst); + auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst); - for(auto & info: allEntries) - { + for (auto& info : allEntries) { #if defined Q_OS_WIN32 QString nativePath = QDir::toNativeSeparators(info.absoluteFilePath()); auto wString = nativePath.toStdWString(); DWORD dwAttrs = GetFileAttributesW(wString.c_str()); // Windows: check for junctions, reparse points and other nasty things of that sort - if(dwAttrs & FILE_ATTRIBUTE_REPARSE_POINT) - { - if (info.isFile()) - { + if (dwAttrs & FILE_ATTRIBUTE_REPARSE_POINT) { + if (info.isFile()) { OK &= QFile::remove(info.absoluteFilePath()); - } - else if (info.isDir()) - { + } else if (info.isDir()) { OK &= dir.rmdir(info.absoluteFilePath()); } } #else // We do not trust Qt with reparse points, but do trust it with unix symlinks. - if(info.isSymLink()) - { + if (info.isSymLink()) { OK &= QFile::remove(info.absoluteFilePath()); } #endif - else if (info.isDir()) - { + else if (info.isDir()) { OK &= deletePath(info.absoluteFilePath()); - } - else if (info.isFile()) - { + } else if (info.isFile()) { OK &= QFile::remove(info.absoluteFilePath()); - } - else - { + } else { OK = false; qCritical() << "Delete ERROR: Unknown filesystem object:" << info.absoluteFilePath(); } @@ -265,22 +226,30 @@ bool deletePath(QString path) return OK; } +bool trash(QString path, QString *pathInTrash = nullptr) +{ +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + return false; +#else + return QFile::moveToTrash(path, pathInTrash); +#endif +} -QString PathCombine(const QString & path1, const QString & path2) +QString PathCombine(const QString& path1, const QString& path2) { - if(!path1.size()) + if (!path1.size()) return path2; - if(!path2.size()) + if (!path2.size()) return path1; return QDir::cleanPath(path1 + QDir::separator() + path2); } -QString PathCombine(const QString & path1, const QString & path2, const QString & path3) +QString PathCombine(const QString& path1, const QString& path2, const QString& path3) { return PathCombine(PathCombine(path1, path2), path3); } -QString PathCombine(const QString & path1, const QString & path2, const QString & path3, const QString & path4) +QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4) { return PathCombine(PathCombine(path1, path2, path3), path4); } @@ -292,17 +261,14 @@ QString AbsolutePath(QString path) QString ResolveExecutable(QString path) { - if (path.isEmpty()) - { + if (path.isEmpty()) { return QString(); } - if(!path.contains('/')) - { + if (!path.contains('/')) { path = QStandardPaths::findExecutable(path); } QFileInfo pathInfo(path); - if(!pathInfo.exists() || !pathInfo.isExecutable()) - { + if (!pathInfo.exists() || !pathInfo.isExecutable()) { return QString(); } return pathInfo.absoluteFilePath(); @@ -322,12 +288,9 @@ QString NormalizePath(QString path) QDir b(path); QString newAbsolute = b.absolutePath(); - if (newAbsolute.startsWith(currentAbsolute)) - { + if (newAbsolute.startsWith(currentAbsolute)) { return a.relativeFilePath(newAbsolute); - } - else - { + } else { return newAbsolute; } } @@ -336,10 +299,8 @@ QString badFilenameChars = "\"\\/?<>:;*|!+\r\n"; QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) { - for (int i = 0; i < string.length(); i++) - { - if (badFilenameChars.contains(string[i])) - { + for (int i = 0; i < string.length(); i++) { + if (badFilenameChars.contains(string[i])) { string[i] = replaceWith; } } @@ -351,15 +312,12 @@ QString DirNameFromString(QString string, QString inDir) int num = 0; QString baseName = RemoveInvalidFilenameChars(string, '-'); QString dirName; - do - { - if(num == 0) - { + do { + if (num == 0) { dirName = baseName; - } - else - { - dirName = baseName + QString::number(num);; + } else { + dirName = baseName + QString::number(num); + ; } // If it's over 9000 @@ -383,36 +341,31 @@ bool checkProblemticPathJava(QDir folder) bool called_coinit = false; -HRESULT CreateLink(LPCSTR linkPath, LPCSTR targetPath, LPCSTR args) +HRESULT CreateLink(LPCCH linkPath, LPCWSTR targetPath, LPCWSTR args) { HRESULT hres; - if (!called_coinit) - { + if (!called_coinit) { hres = CoInitialize(NULL); called_coinit = true; - if (!SUCCEEDED(hres)) - { + if (!SUCCEEDED(hres)) { qWarning("Failed to initialize COM. Error 0x%08lX", hres); return hres; } } - IShellLinkA *link; - hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, - (LPVOID *)&link); + IShellLink* link; + hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&link); - if (SUCCEEDED(hres)) - { - IPersistFile *persistFile; + if (SUCCEEDED(hres)) { + IPersistFile* persistFile; link->SetPath(targetPath); link->SetArguments(args); - hres = link->QueryInterface(IID_IPersistFile, (LPVOID *)&persistFile); - if (SUCCEEDED(hres)) - { + hres = link->QueryInterface(IID_IPersistFile, (LPVOID*)&persistFile); + if (SUCCEEDED(hres)) { WCHAR wstr[MAX_PATH]; MultiByteToWideChar(CP_ACP, 0, linkPath, -1, wstr, MAX_PATH); @@ -433,8 +386,7 @@ QString getDesktopDir() } // Cross-platform Shortcut creation -bool createShortCut(QString location, QString dest, QStringList args, QString name, - QString icon) +bool createShortCut(QString location, QString dest, QStringList args, QString name, QString icon) { #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) location = PathCombine(location, name + ".desktop"); @@ -459,8 +411,7 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na stream.flush(); f.close(); - f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | - QFileDevice::ExeOther); + f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); return true; #elif defined Q_OS_WIN @@ -516,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/FileSystem.h b/launcher/FileSystem.h index fd305b01..b46f3281 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -41,29 +41,27 @@ #include <QDir> #include <QFlags> -namespace FS -{ +namespace FS { -class FileSystemException : public ::Exception -{ -public: - FileSystemException(const QString &message) : Exception(message) {} +class FileSystemException : public ::Exception { + public: + FileSystemException(const QString& message) : Exception(message) {} }; /** * write data to a file safely */ -void write(const QString &filename, const QByteArray &data); +void write(const QString& filename, const QByteArray& data); /** * read data from a file safely\ */ -QByteArray read(const QString &filename); +QByteArray read(const QString& filename); /** * Update the last changed timestamp of an existing file */ -bool updateTimestamp(const QString & filename); +bool updateTimestamp(const QString& filename); /** * Creates all the folders in a path for the specified path @@ -77,35 +75,31 @@ bool ensureFilePathExists(QString filenamepath); */ bool ensureFolderPathExists(QString filenamepath); -class copy -{ -public: - copy(const QString & src, const QString & dst) +class copy { + public: + copy(const QString& src, const QString& dst) { m_src.setPath(src); m_dst.setPath(dst); } - copy & followSymlinks(const bool follow) + copy& followSymlinks(const bool follow) { m_followSymlinks = follow; return *this; } - copy & blacklist(const IPathMatcher * filter) + copy& blacklist(const IPathMatcher* filter) { m_blacklist = filter; return *this; } - bool operator()() - { - return operator()(QString()); - } + bool operator()() { return operator()(QString()); } -private: - bool operator()(const QString &offset); + private: + bool operator()(const QString& offset); -private: + private: bool m_followSymlinks = true; - const IPathMatcher * m_blacklist = nullptr; + const IPathMatcher* m_blacklist = nullptr; QDir m_src; QDir m_dst; }; @@ -115,9 +109,14 @@ private: */ bool deletePath(QString path); -QString PathCombine(const QString &path1, const QString &path2); -QString PathCombine(const QString &path1, const QString &path2, const QString &path3); -QString PathCombine(const QString &path1, const QString &path2, const QString &path3, const QString &path4); +/** + * Trash a folder / file + */ +bool trash(QString path, QString *pathInTrash); + +QString PathCombine(const QString& path1, const QString& path2); +QString PathCombine(const QString& path1, const QString& path2, const QString& path3); +QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4); QString AbsolutePath(QString path); 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/InstanceImportTask.h b/launcher/InstanceImportTask.h index b67d48f3..48ba2161 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -44,7 +44,7 @@ #include "QObjectPtr.h" #include "modplatform/flame/PackManifest.h" -#include <nonstd/optional> +#include <optional> class QuaZip; namespace Flame @@ -90,8 +90,8 @@ private: /* data */ QString m_archivePath; bool m_downloadRequired = false; std::unique_ptr<QuaZip> m_packZip; - QFuture<nonstd::optional<QStringList>> m_extractFuture; - QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; + QFuture<std::optional<QStringList>> m_extractFuture; + QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher; QVector<Flame::File> m_blockedMods; enum class ModpackType{ Unknown, diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index fb7103dd..4447a17c 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -33,30 +33,32 @@ * limitations under the License. */ +#include <QDebug> #include <QDir> #include <QDirIterator> -#include <QSet> #include <QFile> -#include <QThread> -#include <QTextStream> -#include <QXmlStreamReader> -#include <QTimer> -#include <QDebug> #include <QFileSystemWatcher> -#include <QUuid> #include <QJsonArray> #include <QJsonDocument> #include <QMimeData> +#include <QSet> +#include <QStack> +#include <QPair> +#include <QTextStream> +#include <QThread> +#include <QTimer> +#include <QUuid> +#include <QXmlStreamReader> -#include "InstanceList.h" #include "BaseInstance.h" +#include "ExponentialSeries.h" +#include "FileSystem.h" +#include "InstanceList.h" #include "InstanceTask.h" -#include "settings/INISettingsObject.h" #include "NullInstance.h" -#include "minecraft/MinecraftInstance.h" -#include "FileSystem.h" -#include "ExponentialSeries.h" #include "WatchLock.h" +#include "minecraft/MinecraftInstance.h" +#include "settings/INISettingsObject.h" #ifdef Q_OS_WIN32 #include <Windows.h> @@ -64,13 +66,12 @@ const static int GROUP_FILE_FORMAT_VERSION = 1; -InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent) +InstanceList::InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent) : QAbstractListModel(parent), m_globalSettings(settings) { resumeWatch(); // Create aand normalize path - if (!QDir::current().exists(instDir)) - { + if (!QDir::current().exists(instDir)) { QDir::current().mkpath(instDir); } @@ -83,9 +84,7 @@ InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, m_watcher->addPath(m_instDir); } -InstanceList::~InstanceList() -{ -} +InstanceList::~InstanceList() {} Qt::DropActions InstanceList::supportedDragActions() const { @@ -99,7 +98,7 @@ Qt::DropActions InstanceList::supportedDropActions() const bool InstanceList::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const { - if(data && data->hasFormat("application/x-instanceid")) { + if (data && data->hasFormat("application/x-instanceid")) { return true; } return false; @@ -107,7 +106,7 @@ bool InstanceList::canDropMimeData(const QMimeData* data, Qt::DropAction action, bool InstanceList::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) { - if(data && data->hasFormat("application/x-instanceid")) { + if (data && data->hasFormat("application/x-instanceid")) { return true; } return false; @@ -120,35 +119,33 @@ QStringList InstanceList::mimeTypes() const return types; } -QMimeData * InstanceList::mimeData(const QModelIndexList& indexes) const +QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const { auto mimeData = QAbstractListModel::mimeData(indexes); - if(indexes.size() == 1) { + if (indexes.size() == 1) { auto instanceId = data(indexes[0], InstanceIDRole).toString(); mimeData->setData("application/x-instanceid", instanceId.toUtf8()); } return mimeData; } - -int InstanceList::rowCount(const QModelIndex &parent) const +int InstanceList::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); return m_instances.count(); } -QModelIndex InstanceList::index(int row, int column, const QModelIndex &parent) const +QModelIndex InstanceList::index(int row, int column, const QModelIndex& parent) const { Q_UNUSED(parent); if (row < 0 || row >= m_instances.size()) return QModelIndex(); - return createIndex(row, column, (void *)m_instances.at(row).get()); + return createIndex(row, column, (void*)m_instances.at(row).get()); } -QVariant InstanceList::data(const QModelIndex &index, int role) const +QVariant InstanceList::data(const QModelIndex& index, int role) const { - if (!index.isValid()) - { + if (!index.isValid()) { return QVariant(); } BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer()); @@ -193,29 +190,25 @@ QVariant InstanceList::data(const QModelIndex &index, int role) const bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role) { - if (!index.isValid()) - { + if (!index.isValid()) { return false; } - if(role != Qt::EditRole) - { + if (role != Qt::EditRole) { return false; } - BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer()); + BaseInstance* pdata = static_cast<BaseInstance*>(index.internalPointer()); auto newName = value.toString(); - if(pdata->name() == newName) - { + if (pdata->name() == newName) { return true; } pdata->setName(newName); return true; } -Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const +Qt::ItemFlags InstanceList::flags(const QModelIndex& index) const { Qt::ItemFlags f; - if (index.isValid()) - { + if (index.isValid()) { f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); } return f; @@ -224,13 +217,11 @@ Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const GroupId InstanceList::getInstanceGroup(const InstanceId& id) const { auto inst = getInstanceById(id); - if(!inst) - { + if (!inst) { return GroupId(); } auto iter = m_instanceGroupIndex.find(inst->id()); - if(iter != m_instanceGroupIndex.end()) - { + if (iter != m_instanceGroupIndex.end()) { return *iter; } return GroupId(); @@ -239,33 +230,27 @@ GroupId InstanceList::getInstanceGroup(const InstanceId& id) const void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name) { auto inst = getInstanceById(id); - if(!inst) - { + if (!inst) { qDebug() << "Attempt to set a null instance's group"; return; } bool changed = false; auto iter = m_instanceGroupIndex.find(inst->id()); - if(iter != m_instanceGroupIndex.end()) - { - if(*iter != name) - { + if (iter != m_instanceGroupIndex.end()) { + if (*iter != name) { *iter = name; changed = true; } - } - else - { + } else { changed = true; m_instanceGroupIndex[id] = name; } - if(changed) - { + if (changed) { m_groupNameCache.insert(name); auto idx = getInstIndex(inst.get()); - emit dataChanged(index(idx), index(idx), {GroupRole}); + emit dataChanged(index(idx), index(idx), { GroupRole }); saveGroupList(); } } @@ -279,24 +264,20 @@ void InstanceList::deleteGroup(const QString& name) { bool removed = false; qDebug() << "Delete group" << name; - for(auto & instance: m_instances) - { - const auto & instID = instance->id(); + for (auto& instance : m_instances) { + const auto& instID = instance->id(); auto instGroupName = getInstanceGroup(instID); - if(instGroupName == name) - { + if (instGroupName == name) { m_instanceGroupIndex.remove(instID); qDebug() << "Remove" << instID << "from group" << name; removed = true; auto idx = getInstIndex(instance.get()); - if(idx > 0) - { - emit dataChanged(index(idx), index(idx), {GroupRole}); + if (idx > 0) { + emit dataChanged(index(idx), index(idx), { GroupRole }); } } } - if(removed) - { + if (removed) { saveGroupList(); } } @@ -306,23 +287,75 @@ bool InstanceList::isGroupCollapsed(const QString& group) return m_collapsedGroups.contains(group); } +bool InstanceList::trashInstance(const InstanceId& id) +{ + auto inst = getInstanceById(id); + if (!inst) { + qDebug() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?)."; + return false; + } + + auto cachedGroupId = m_instanceGroupIndex[id]; + + qDebug() << "Will trash instance" << id; + QString trashedLoc; + + if (m_instanceGroupIndex.remove(id)) { + saveGroupList(); + } + + if (!FS::trash(inst->instanceRoot(), &trashedLoc)) { + qDebug() << "Trash of instance" << id << "has not been completely successfully..."; + return false; + } + + qDebug() << "Instance" << id << "has been trashed by the launcher."; + m_trashHistory.push({id, inst->instanceRoot(), trashedLoc, cachedGroupId}); + + return true; +} + +bool InstanceList::trashedSomething() { + return !m_trashHistory.empty(); +} + +void InstanceList::undoTrashInstance() { + if (m_trashHistory.empty()) { + qWarning() << "Nothing to recover from trash."; + return; + } + + auto top = m_trashHistory.pop(); + + while (QDir(top.polyPath).exists()) { + top.id += "1"; + top.polyPath += "1"; + } + + qDebug() << "Moving" << top.trashPath << "back to" << top.polyPath; + QFile(top.trashPath).rename(top.polyPath); + + m_instanceGroupIndex[top.id] = top.groupName; + m_groupNameCache.insert(top.groupName); + + saveGroupList(); + emit instancesChanged(); +} + void InstanceList::deleteInstance(const InstanceId& id) { auto inst = getInstanceById(id); - if(!inst) - { + if (!inst) { qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; return; } - if(m_instanceGroupIndex.remove(id)) - { + if (m_instanceGroupIndex.remove(id)) { saveGroupList(); } qDebug() << "Will delete instance" << id; - if(!FS::deletePath(inst->instanceRoot())) - { + if (!FS::deletePath(inst->instanceRoot())) { qWarning() << "Deletion of instance" << id << "has not been completely successful ..."; return; } @@ -330,15 +363,13 @@ void InstanceList::deleteInstance(const InstanceId& id) qDebug() << "Instance" << id << "has been deleted by the launcher."; } -static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr> &list) +static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr>& list) { QMap<InstanceId, InstanceLocator> out; int i = 0; - for(auto & item: list) - { + for (auto& item : list) { auto id = item->id(); - if(out.contains(id)) - { + if (out.contains(id)) { qWarning() << "Duplicate ID" << id << "in instance list"; } out[id] = std::make_pair(item, i); @@ -347,24 +378,21 @@ static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr> & return out; } -QList< InstanceId > InstanceList::discoverInstances() +QList<InstanceId> InstanceList::discoverInstances() { qDebug() << "Discovering instances in" << m_instDir; QList<InstanceId> out; QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); - while (iter.hasNext()) - { + while (iter.hasNext()) { QString subDir = iter.next(); QFileInfo dirInfo(subDir); if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists()) continue; // if it is a symlink, ignore it if it goes to the instance folder - if(dirInfo.isSymLink()) - { + if (dirInfo.isSymLink()) { QFileInfo targetInfo(dirInfo.symLinkTarget()); QFileInfo instDirInfo(m_instDir); - if(targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) - { + if (targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) { qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder"; continue; } @@ -388,74 +416,56 @@ InstanceList::InstListError InstanceList::loadList() QList<InstancePtr> newList; - for(auto & id: discoverInstances()) - { - if(existingIds.contains(id)) - { + for (auto& id : discoverInstances()) { + if (existingIds.contains(id)) { auto instPair = existingIds[id]; existingIds.remove(id); qDebug() << "Should keep and soft-reload" << id; - } - else - { + } else { InstancePtr instPtr = loadInstance(id); - if(instPtr) - { + if (instPtr) { newList.append(instPtr); } } } // TODO: looks like a general algorithm with a few specifics inserted. Do something about it. - if(!existingIds.isEmpty()) - { + if (!existingIds.isEmpty()) { // get the list of removed instances and sort it by their original index, from last to first auto deadList = existingIds.values(); - auto orderSortPredicate = [](const InstanceLocator & a, const InstanceLocator & b) -> bool - { - return a.second > b.second; - }; + auto orderSortPredicate = [](const InstanceLocator& a, const InstanceLocator& b) -> bool { return a.second > b.second; }; std::sort(deadList.begin(), deadList.end(), orderSortPredicate); // remove the contiguous ranges of rows int front_bookmark = -1; int back_bookmark = -1; int currentItem = -1; - auto removeNow = [&]() - { + auto removeNow = [&]() { beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark); m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1); endRemoveRows(); front_bookmark = -1; back_bookmark = currentItem; }; - for(auto & removedItem: deadList) - { + for (auto& removedItem : deadList) { auto instPtr = removedItem.first; instPtr->invalidate(); currentItem = removedItem.second; - if(back_bookmark == -1) - { + if (back_bookmark == -1) { // no bookmark yet back_bookmark = currentItem; - } - else if(currentItem == front_bookmark - 1) - { + } else if (currentItem == front_bookmark - 1) { // part of contiguous sequence, continue - } - else - { + } else { // seam between previous and current item removeNow(); } front_bookmark = currentItem; } - if(back_bookmark != -1) - { + if (back_bookmark != -1) { removeNow(); } } - if(newList.size()) - { + if (newList.size()) { add(newList); } m_dirty = false; @@ -466,26 +476,23 @@ InstanceList::InstListError InstanceList::loadList() void InstanceList::updateTotalPlayTime() { totalPlayTime = 0; - for(auto const& itr : m_instances) - { + for (auto const& itr : m_instances) { totalPlayTime += itr.get()->totalTimePlayed(); } } void InstanceList::saveNow() { - for(auto & item: m_instances) - { + for (auto& item : m_instances) { item->saveNow(); } } -void InstanceList::add(const QList<InstancePtr> &t) +void InstanceList::add(const QList<InstancePtr>& t) { beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1); m_instances.append(t); - for(auto & ptr : t) - { + for (auto& ptr : t) { connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); } endInsertRows(); @@ -493,69 +500,61 @@ void InstanceList::add(const QList<InstancePtr> &t) void InstanceList::resumeWatch() { - if(m_watchLevel > 0) - { + if (m_watchLevel > 0) { qWarning() << "Bad suspend level resume in instance list"; return; } m_watchLevel++; - if(m_watchLevel > 0 && m_dirty) - { + if (m_watchLevel > 0 && m_dirty) { loadList(); } } void InstanceList::suspendWatch() { - m_watchLevel --; + m_watchLevel--; } void InstanceList::providerUpdated() { m_dirty = true; - if(m_watchLevel == 1) - { + if (m_watchLevel == 1) { loadList(); } } InstancePtr InstanceList::getInstanceById(QString instId) const { - if(instId.isEmpty()) + if (instId.isEmpty()) return InstancePtr(); - for(auto & inst: m_instances) - { - if (inst->id() == instId) - { + for (auto& inst : m_instances) { + if (inst->id() == instId) { return inst; } } return InstancePtr(); } -QModelIndex InstanceList::getInstanceIndexById(const QString &id) const +QModelIndex InstanceList::getInstanceIndexById(const QString& id) const { return index(getInstIndex(getInstanceById(id).get())); } -int InstanceList::getInstIndex(BaseInstance *inst) const +int InstanceList::getInstIndex(BaseInstance* inst) const { int count = m_instances.count(); - for (int i = 0; i < count; i++) - { - if (inst == m_instances[i].get()) - { + for (int i = 0; i < count; i++) { + if (inst == m_instances[i].get()) { return i; } } return -1; } -void InstanceList::propertiesChanged(BaseInstance *inst) +void InstanceList::propertiesChanged(BaseInstance* inst) { int i = getInstIndex(inst); - if (i != -1) - { + if (i != -1) { emit dataChanged(index(i), index(i)); updateTotalPlayTime(); } @@ -563,8 +562,7 @@ void InstanceList::propertiesChanged(BaseInstance *inst) InstancePtr InstanceList::loadInstance(const InstanceId& id) { - if(!m_groupsLoaded) - { + if (!m_groupsLoaded) { loadGroupList(); } @@ -592,50 +590,42 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) void InstanceList::saveGroupList() { qDebug() << "Will save group list now."; - if(!m_instancesProbed) - { + if (!m_instancesProbed) { qDebug() << "Group saving prevented because we don't know the full list of instances yet."; return; } WatchLock foo(m_watcher, m_instDir); QString groupFileName = m_instDir + "/instgroups.json"; QMap<QString, QSet<QString>> reverseGroupMap; - for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) - { + for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) { QString id = iter.key(); QString group = iter.value(); if (group.isEmpty()) continue; - if(!instanceSet.contains(id)) - { + if (!instanceSet.contains(id)) { qDebug() << "Skipping saving missing instance" << id << "to groups list."; continue; } - if (!reverseGroupMap.count(group)) - { + if (!reverseGroupMap.count(group)) { QSet<QString> set; set.insert(id); reverseGroupMap[group] = set; - } - else - { - QSet<QString> &set = reverseGroupMap[group]; + } else { + QSet<QString>& set = reverseGroupMap[group]; set.insert(id); } } QJsonObject toplevel; toplevel.insert("formatVersion", QJsonValue(QString("1"))); QJsonObject groupsArr; - for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++) - { + for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++) { auto list = iter.value(); auto name = iter.key(); QJsonObject groupObj; QJsonArray instanceArr; groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name))); - for (auto item : list) - { + for (auto item : list) { instanceArr.append(QJsonValue(item)); } groupObj.insert("instances", instanceArr); @@ -643,13 +633,10 @@ void InstanceList::saveGroupList() } toplevel.insert("groups", groupsArr); QJsonDocument doc(toplevel); - try - { + try { FS::write(groupFileName, doc.toJson()); qDebug() << "Group list saved."; - } - catch (const FS::FileSystemException &e) - { + } catch (const FS::FileSystemException& e) { qCritical() << "Failed to write instance group file :" << e.cause(); } } @@ -665,12 +652,9 @@ void InstanceList::loadGroupList() return; QByteArray jsonData; - try - { + try { jsonData = FS::read(groupFileName); - } - catch (const FS::FileSystemException &e) - { + } catch (const FS::FileSystemException& e) { qCritical() << "Failed to read instance group file :" << e.cause(); return; } @@ -679,17 +663,15 @@ void InstanceList::loadGroupList() QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error); // if the json was bad, fail - if (error.error != QJsonParseError::NoError) - { + if (error.error != QJsonParseError::NoError) { qCritical() << QString("Failed to parse instance group file: %1 at offset %2") - .arg(error.errorString(), QString::number(error.offset)) - .toUtf8(); + .arg(error.errorString(), QString::number(error.offset)) + .toUtf8(); return; } // if the root of the json wasn't an object, fail - if (!jsonDoc.isObject()) - { + if (!jsonDoc.isObject()) { qWarning() << "Invalid group file. Root entry should be an object."; return; } @@ -701,8 +683,7 @@ void InstanceList::loadGroupList() return; // Get the groups. if it's not an object, fail - if (!rootObj.value("groups").isObject()) - { + if (!rootObj.value("groups").isObject()) { qWarning() << "Invalid group list JSON: 'groups' should be an object."; return; } @@ -712,21 +693,20 @@ void InstanceList::loadGroupList() // Iterate through all the groups. QJsonObject groupMapping = rootObj.value("groups").toObject(); - for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) - { + for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) { QString groupName = iter.key(); // If not an object, complain and skip to the next one. - if (!iter.value().isObject()) - { + if (!iter.value().isObject()) { qWarning() << QString("Group '%1' in the group list should be an object.").arg(groupName).toUtf8(); continue; } QJsonObject groupObj = iter.value().toObject(); - if (!groupObj.value("instances").isArray()) - { - qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.").arg(groupName).toUtf8(); + if (!groupObj.value("instances").isArray()) { + qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.") + .arg(groupName) + .toUtf8(); continue; } @@ -734,15 +714,14 @@ void InstanceList::loadGroupList() groupSet.insert(groupName); auto hidden = groupObj.value("hidden").toBool(false); - if(hidden) { + if (hidden) { m_collapsedGroups.insert(groupName); } // Iterate through the list of instances in the group. QJsonArray instancesArray = groupObj.value("instances").toArray(); - for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++) - { + for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++) { m_instanceGroupIndex[(*iter2).toString()] = groupName; } } @@ -757,13 +736,11 @@ void InstanceList::instanceDirContentsChanged(const QString& path) emit instancesChanged(); } -void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value) +void InstanceList::on_InstFolderChanged(const Setting& setting, QVariant value) { QString newInstDir = QDir(value.toString()).canonicalPath(); - if(newInstDir != m_instDir) - { - if(m_groupsLoaded) - { + if (newInstDir != m_instDir) { + if (m_groupsLoaded) { saveGroupList(); } m_instDir = newInstDir; @@ -775,7 +752,7 @@ void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value) void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed) { qDebug() << "Group" << group << (collapsed ? "collapsed" : "expanded"); - if(collapsed) { + if (collapsed) { m_collapsedGroups.insert(group); } else { m_collapsedGroups.remove(group); @@ -783,19 +760,14 @@ void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed) saveGroupList(); } -class InstanceStaging : public Task -{ -Q_OBJECT +class InstanceStaging : public Task { + Q_OBJECT const unsigned minBackoff = 1; const unsigned maxBackoff = 16; -public: - InstanceStaging ( - InstanceList * parent, - Task * child, - const QString & stagingPath, - const QString& instanceName, - const QString& groupName ) - : backoff(minBackoff, maxBackoff) + + public: + InstanceStaging(InstanceList* parent, Task* child, const QString& stagingPath, const QString& instanceName, const QString& groupName) + : backoff(minBackoff, maxBackoff) { m_parent = parent; m_child.reset(child); @@ -810,62 +782,51 @@ public: connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded); } - virtual ~InstanceStaging() {}; - + virtual ~InstanceStaging(){}; // FIXME/TODO: add ability to abort during instance commit retries bool abort() override { - if(m_child && m_child->canAbort()) - { + if (m_child && m_child->canAbort()) { return m_child->abort(); } return false; } bool canAbort() const override { - if(m_child && m_child->canAbort()) - { + if (m_child && m_child->canAbort()) { return true; } return false; } -protected: - virtual void executeTask() override - { - m_child->start(); - } - QStringList warnings() const override - { - return m_child->warnings(); - } + protected: + virtual void executeTask() override { m_child->start(); } + QStringList warnings() const override { return m_child->warnings(); } -private slots: + private slots: void childSucceded() { unsigned sleepTime = backoff(); - if(m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName)) - { + if (m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName)) { emitSucceeded(); return; } // we actually failed, retry? - if(sleepTime == maxBackoff) - { + if (sleepTime == maxBackoff) { emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something.")); return; } qDebug() << "Failed to commit instance" << m_instanceName << "Initiating backoff:" << sleepTime; m_backoffTimer.start(sleepTime * 500); } - void childFailed(const QString & reason) + void childFailed(const QString& reason) { m_parent->destroyStagingPath(m_stagingPath); emitFailed(reason); } -private: + private: /* * WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows. * Basically, it starts messing things up while the launcher is extracting/creating instances @@ -873,14 +834,14 @@ private: */ ExponentialSeries backoff; QString m_stagingPath; - InstanceList * m_parent; + InstanceList* m_parent; unique_qobject_ptr<Task> m_child; QString m_instanceName; QString m_groupName; QTimer m_backoffTimer; }; -Task * InstanceList::wrapInstanceTask(InstanceTask * task) +Task* InstanceList::wrapInstanceTask(InstanceTask* task) { auto stagingPath = getStagedInstancePath(); task->setStagingPath(stagingPath); @@ -895,8 +856,7 @@ QString InstanceList::getStagedInstancePath() QString relPath = FS::PathCombine(tempDir, key); QDir rootPath(m_instDir); auto path = FS::PathCombine(m_instDir, relPath); - if(!rootPath.mkpath(relPath)) - { + if (!rootPath.mkpath(relPath)) { return QString(); } #ifdef Q_OS_WIN32 @@ -913,8 +873,7 @@ bool InstanceList::commitStagedInstance(const QString& path, const QString& inst { WatchLock lock(m_watcher, m_instDir); QString destination = FS::PathCombine(m_instDir, instID); - if(!dir.rename(path, destination)) - { + if (!dir.rename(path, destination)) { qWarning() << "Failed to move" << path << "to" << destination; return false; } @@ -933,7 +892,8 @@ bool InstanceList::destroyStagingPath(const QString& keyPath) return FS::deletePath(keyPath); } -int InstanceList::getTotalPlayTime() { +int InstanceList::getTotalPlayTime() +{ updateTotalPlayTime(); return totalPlayTime; } diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index bc6c3af0..62282f04 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -19,6 +19,8 @@ #include <QAbstractListModel> #include <QSet> #include <QList> +#include <QStack> +#include <QPair> #include "BaseInstance.h" @@ -46,6 +48,12 @@ enum class GroupsState Dirty }; +struct TrashHistoryItem { + QString id; + QString polyPath; + QString trashPath; + QString groupName; +}; class InstanceList : public QAbstractListModel { @@ -102,6 +110,9 @@ public: void setInstanceGroup(const InstanceId & id, const GroupId& name); void deleteGroup(const GroupId & name); + bool trashInstance(const InstanceId &id); + bool trashedSomething(); + void undoTrashInstance(); void deleteInstance(const InstanceId & id); // Wrap an instance creation task in some more task machinery and make it ready to be used @@ -180,4 +191,6 @@ private: QSet<InstanceId> instanceSet; bool m_groupsLoaded = false; bool m_instancesProbed = false; + + QStack<TrashHistoryItem> m_trashHistory; }; 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 8518e606..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 @@ -268,7 +268,7 @@ bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & re // ours -nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) +std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) { QDir directory(target); QStringList extracted; @@ -277,7 +277,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & auto numEntries = zip->getEntriesCount(); if(numEntries < 0) { qWarning() << "Failed to enumerate files in archive"; - return nonstd::nullopt; + return std::nullopt; } else if(numEntries == 0) { qDebug() << "Extracting empty archives seems odd..."; @@ -286,7 +286,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & else if (!zip->goToFirstFile()) { qWarning() << "Failed to seek to first file in zip"; - return nonstd::nullopt; + return std::nullopt; } do @@ -323,7 +323,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & { qWarning() << "Failed to extract file" << original_name << "to" << absFilePath; JlCompress::removeFile(extracted); - return nonstd::nullopt; + return std::nullopt; } extracted.append(absFilePath); @@ -341,7 +341,7 @@ bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &tar } // ours -nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir) +std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir) { QuaZip zip(fileCompressed); if (!zip.open(QuaZip::mdUnzip)) @@ -352,13 +352,13 @@ nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString return QStringList(); } qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; - return nonstd::nullopt; + return std::nullopt; } return MMCZip::extractSubDir(&zip, "", dir); } // ours -nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) +std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) { QuaZip zip(fileCompressed); if (!zip.open(QuaZip::mdUnzip)) @@ -369,7 +369,7 @@ nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString return QStringList(); } qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; - return nonstd::nullopt; + return std::nullopt; } return MMCZip::extractSubDir(&zip, subdir, dir); } diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index 7f43d158..ce9775bd 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -42,7 +42,7 @@ #include <functional> #include <quazip/JlCompress.h> -#include <nonstd/optional> +#include <optional> namespace MMCZip { @@ -95,7 +95,7 @@ namespace MMCZip /** * Extract a subdirectory from an archive */ - nonstd::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); + std::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); bool extractRelFile(QuaZip *zip, const QString & file, const QString &target); @@ -106,7 +106,7 @@ namespace MMCZip * \param dir The directory to extract to, the current directory if left empty. * \return The list of the full paths of the files extracted, empty on failure. */ - nonstd::optional<QStringList> extractDir(QString fileCompressed, QString dir); + std::optional<QStringList> extractDir(QString fileCompressed, QString dir); /** * Extract a subdirectory from an archive @@ -116,7 +116,7 @@ namespace MMCZip * \param dir The directory to extract to, the current directory if left empty. * \return The list of the full paths of the files extracted, empty on failure. */ - nonstd::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir); + std::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir); /** * Extract a single file from an archive into a directory 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/meta/VersionList.cpp b/launcher/meta/VersionList.cpp index 6d23ce9a..f609e94c 100644 --- a/launcher/meta/VersionList.cpp +++ b/launcher/meta/VersionList.cpp @@ -140,6 +140,13 @@ VersionPtr VersionList::getVersion(const QString &version) return out; } +bool VersionList::hasVersion(QString version) const +{ + auto ver = std::find_if(m_versions.constBegin(), m_versions.constEnd(), + [&](Meta::VersionPtr const& a){ return a->version() == version; }); + return (ver != m_versions.constEnd()); +} + void VersionList::setName(const QString &name) { m_name = name; diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h index 378255df..a6db2fd7 100644 --- a/launcher/meta/VersionList.h +++ b/launcher/meta/VersionList.h @@ -66,6 +66,7 @@ public: QString humanReadable() const; VersionPtr getVersion(const QString &version); + bool hasVersion(QString version) const; QVector<VersionPtr> versions() const { 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/World.cpp b/launcher/minecraft/World.cpp index dfcb43d8..90fcf337 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -53,12 +53,12 @@ #include <QCoreApplication> -#include <nonstd/optional> +#include <optional> -using nonstd::optional; -using nonstd::nullopt; +using std::optional; +using std::nullopt; -GameType::GameType(nonstd::optional<int> original): +GameType::GameType(std::optional<int> original): original(original) { if(!original) { diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h index 0f587620..8327253a 100644 --- a/launcher/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -16,11 +16,11 @@ #pragma once #include <QFileInfo> #include <QDateTime> -#include <nonstd/optional> +#include <optional> struct GameType { GameType() = default; - GameType (nonstd::optional<int> original); + GameType (std::optional<int> original); QString toTranslatedString() const; QString toLogString() const; @@ -33,7 +33,7 @@ struct GameType { Adventure, Spectator } type = Unknown; - nonstd::optional<int> original; + std::optional<int> original; }; class World 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..13fed1c9 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,142 @@ 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 { - Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); - auto flags = defaultFlags; - if(interaction_disabled) { - flags &= ~Qt::ItemIsDropEnabled; - } - else - { - flags |= Qt::ItemIsDropEnabled; - if(index.isValid()) { - flags |= Qt::ItemIsUserCheckable; - } - } - return flags; + return NUM_COLUMNS; } -Qt::DropActions ModFolderModel::supportedDropActions() const +Task* ModFolderModel::createUpdateTask() { - // copy from outside, move from within and other mod lists - return Qt::CopyAction | Qt::MoveAction; + auto index_dir = indexDir(); + auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load); + m_first_folder_load = false; + return task; } -QStringList ModFolderModel::mimeTypes() const +Task* ModFolderModel::createParseTask(Resource& resource) { - QStringList types; - types << "text/uri-list"; - return types; + return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo()); } -bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) +bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata) { - if (action == Qt::IgnoreAction) - { - return true; + for(auto mod : allMods()){ + if(mod->fileinfo().fileName() == filename){ + auto index_dir = indexDir(); + mod->destroy(index_dir, preserve_metadata); + + update(); + + return true; + } } - // check if the action is supported - if (!data || !(action & supportedDropActions())) - { + return false; +} + +bool ModFolderModel::deleteMods(const QModelIndexList& indexes) +{ + if(!m_can_interact) { return false; } - // files dropped from outside? - if (data->hasUrls()) + if(indexes.isEmpty()) + return true; + + for (auto i: indexes) { - 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()); + if(i.column() != 0) { + continue; } - return true; + auto m = at(i.row()); + auto index_dir = indexDir(); + m->destroy(index_dir); } - return false; + + update(); + + return true; +} + +bool ModFolderModel::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +bool ModFolderModel::startWatching() +{ + // Remove orphaned metadata next time + m_first_folder_load = true; + return ResourceFolderModel::startWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); +} + +bool ModFolderModel::stopWatching() +{ + return ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); +} + +auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod*> +{ + QList<Mod*> selected_resources; + for (auto i : indexes) { + if(i.column() != 0) + continue; + + selected_resources.push_back(at(i.row())); + } + return selected_resources; +} + +auto ModFolderModel::allMods() -> QList<Mod*> +{ + QList<Mod*> mods; + + for (auto& res : m_resources) { + mods.append(static_cast<Mod*>(res.get())); + } + + 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); +} + +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..93980319 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&) 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..f9bd811e --- /dev/null +++ b/launcher/minecraft/mod/Resource.h @@ -0,0 +1,117 @@ +#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, + PACK_FORMAT +}; + +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 isResolved() const -> bool { return m_is_resolved; } + [[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..45d1db59 --- /dev/null +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -0,0 +1,524 @@ +#include "ResourceFolderModel.h" + +#include <QCoreApplication> +#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); +} + +ResourceFolderModel::~ResourceFolderModel() +{ + while (!QThreadPool::globalInstance()->waitForDone(100)) + QCoreApplication::processEvents(); +} + +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); + connect(m_current_update_task.get(), &Task::finished, this, [=] { + m_current_update_task.reset(); + if (m_scheduled_update) { + m_scheduled_update = false; + update(); + } else { + emit updateFinished(); + } + }, Qt::ConnectionType::QueuedConnection); + + QThreadPool::globalInstance()->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; + + int ticket = m_next_resolution_ticket.fetch_add(1); + + 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); + + QThreadPool::globalInstance()->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); +} + +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..5652c156 --- /dev/null +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -0,0 +1,332 @@ +#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); + ~ResourceFolderModel() override; + + /** 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&) { 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; + std::atomic<int> m_next_resolution_ticket = 0; +}; + +/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ +#define RESOURCE_HELPERS(T) \ + [[nodiscard]] T* operator[](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 ticket = current_resource->resolutionTicket(); + if (m_active_parse_tasks.contains(ticket)) { + auto task = (*m_active_parse_tasks.find(ticket)).get(); + task->abort(); + } + } + + m_resources[row].reset(new_resource); + resolveResource(m_resources.at(row)); + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + } + } + + // remove resources no longer present + { + QSet<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 ticket = (*removed_it)->resolutionTicket(); + if (m_active_parse_tasks.contains(ticket)) { + auto task = (*m_active_parse_tasks.find(ticket)).get(); + task->abort(); + } + } + + beginRemoveRows(QModelIndex(), removed_index, removed_index); + m_resources.erase(removed_it); + endRemoveRows(); + } + } + + // add new resources to the end + { + QSet<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/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp new file mode 100644 index 00000000..3fc10a2f --- /dev/null +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -0,0 +1,116 @@ +#include "ResourcePack.h" + +#include <QDebug> +#include <QMap> +#include <QRegularExpression> + +#include "Version.h" + +#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" + +// Values taken from: +// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta +static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = { + { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } }, + { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } }, + { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, + { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, + { 9, { Version("1.19"), Version("1.19.2") } }, +}; + +void ResourcePack::setPackFormat(int new_format_id) +{ + QMutexLocker locker(&m_data_lock); + + if (!s_pack_format_versions.contains(new_format_id)) { + qWarning() << "Pack format '%1' is not a recognized resource pack id!"; + } + + m_pack_format = new_format_id; +} + +void ResourcePack::setDescription(QString new_description) +{ + QMutexLocker locker(&m_data_lock); + + m_description = new_description; +} + +void ResourcePack::setImage(QImage new_image) +{ + QMutexLocker locker(&m_data_lock); + + Q_ASSERT(!new_image.isNull()); + + if (m_pack_image_cache_key.key.isValid()) + QPixmapCache::remove(m_pack_image_cache_key.key); + + m_pack_image_cache_key.key = QPixmapCache::insert(QPixmap::fromImage(new_image)); + m_pack_image_cache_key.was_ever_used = true; +} + +QPixmap ResourcePack::image(QSize size) +{ + QPixmap cached_image; + if (QPixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { + if (size.isNull()) + return cached_image; + return cached_image.scaled(size); + } + + // No valid image we can get + if (!m_pack_image_cache_key.was_ever_used) + return {}; + + // Imaged got evicted from the cache. Re-process it and retry. + ResourcePackUtils::process(*this); + return image(size); +} + +std::pair<Version, Version> ResourcePack::compatibleVersions() const +{ + if (!s_pack_format_versions.contains(m_pack_format)) { + return { {}, {} }; + } + + return s_pack_format_versions.constFind(m_pack_format).value(); +} + +std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type) const +{ + auto const& cast_other = static_cast<ResourcePack const&>(other); + + switch (type) { + default: { + auto res = Resource::compare(other, type); + if (res.first != 0) + return res; + } + case SortType::PACK_FORMAT: { + auto this_ver = packFormat(); + auto other_ver = cast_other.packFormat(); + + if (this_ver > other_ver) + return { 1, type == SortType::PACK_FORMAT }; + if (this_ver < other_ver) + return { -1, type == SortType::PACK_FORMAT }; + } + } + return { 0, false }; +} + +bool ResourcePack::applyFilter(QRegularExpression filter) const +{ + if (filter.match(description()).hasMatch()) + return true; + + if (filter.match(QString::number(packFormat())).hasMatch()) + return true; + + if (filter.match(compatibleVersions().first.toString()).hasMatch()) + return true; + if (filter.match(compatibleVersions().second.toString()).hasMatch()) + return true; + + return Resource::applyFilter(filter); +} diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h new file mode 100644 index 00000000..03121908 --- /dev/null +++ b/launcher/minecraft/mod/ResourcePack.h @@ -0,0 +1,69 @@ +#pragma once + +#include "Resource.h" + +#include <QImage> +#include <QMutex> +#include <QPixmap> +#include <QPixmapCache> + +class Version; + +/* TODO: + * + * Store localized descriptions + * */ + +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) {} + + /** Gets the numerical ID of the pack format. */ + [[nodiscard]] int packFormat() const { return m_pack_format; } + /** Gets, respectively, the lower and upper versions supported by the set pack format. */ + [[nodiscard]] std::pair<Version, Version> compatibleVersions() const; + + /** Gets the description of the resource pack. */ + [[nodiscard]] QString description() const { return m_description; } + + /** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */ + [[nodiscard]] QPixmap image(QSize size); + + /** Thread-safe. */ + void setPackFormat(int new_format_id); + + /** Thread-safe. */ + void setDescription(QString new_description); + + /** Thread-safe. */ + void setImage(QImage new_image); + + [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override; + [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; + + protected: + mutable QMutex m_data_lock; + + /* The 'version' of a resource pack, as defined in the pack.mcmeta file. + * See https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta + */ + int m_pack_format = 0; + + /** The resource pack's description, as defined in the pack.mcmeta file. + */ + QString m_description; + + /** The resource pack's image file cache key, for access in the QPixmapCache global instance. + * + * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), + * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. + */ + struct { + QPixmapCache::Key key; + bool was_ever_used = false; + } m_pack_image_cache_key; +}; diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index 276804ed..f8a6c1cf 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -1,58 +1,151 @@ // 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. -*/ + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * 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 "ResourcePackFolderModel.h" -ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) { +#include "Version.h" + +#include "minecraft/mod/tasks/BasicFolderLoadTask.h" +#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" + +ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir)) +{ + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; } -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(); +QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) { + case Qt::DisplayRole: + switch (column) { + case NameColumn: + return m_resources[row]->name(); + case PackFormatColumn: { + auto resource = at(row); + auto pack_format = resource->packFormat(); + if (pack_format == 0) + return tr("Unrecognized"); + + auto version_bounds = resource->compatibleVersions(); + if (version_bounds.first.toString().isEmpty()) + return QString::number(pack_format); + + return QString("%1 (%2 - %3)") + .arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString()); + } + case DateColumn: + return m_resources[row]->dateTimeChanged(); + + default: + return {}; + } + + case Qt::ToolTipRole: { + if (column == PackFormatColumn) { + //: The string being explained by this is in the format: ID (Lower version - Upper version) + return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); + } + return m_resources[row]->internal_id(); } + case Qt::CheckStateRole: + switch (column) { + case ActiveColumn: + return at(row)->enabled() ? Qt::Checked : Qt::Unchecked; + default: + return {}; + } + default: + return {}; } +} + +QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) { + case Qt::DisplayRole: + switch (section) { + case ActiveColumn: + return QString(); + case NameColumn: + return tr("Name"); + case PackFormatColumn: + return tr("Pack Format"); + case DateColumn: + return tr("Last changed"); + default: + return {}; + } + + case Qt::ToolTipRole: + switch (section) { + case ActiveColumn: + return tr("Is the resource pack enabled? (Only valid for ZIPs)"); + case NameColumn: + return tr("The name of the resource pack."); + case PackFormatColumn: + //: The string being explained by this is in the format: ID (Lower version - Upper version) + return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); + case DateColumn: + return tr("The date and time this resource pack was last changed (or added)."); + default: + return {}; + } + default: + return {}; + } + return {}; +} + +int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const +{ + return NUM_COLUMNS; +} + +Task* ResourcePackFolderModel::createUpdateTask() +{ + return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return new ResourcePack(entry); }); +} - return ModFolderModel::headerData(section, orientation, role); +Task* ResourcePackFolderModel::createParseTask(Resource& resource) +{ + return new LocalResourcePackParseTask(m_next_resolution_ticket, static_cast<ResourcePack&>(resource)); } diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index 0cd6214b..cb620ce2 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.h +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -1,13 +1,31 @@ #pragma once -#include "ModFolderModel.h" +#include "ResourceFolderModel.h" -class ResourcePackFolderModel : public ModFolderModel +#include "ResourcePack.h" + +class ResourcePackFolderModel : public ResourceFolderModel { Q_OBJECT - public: + enum Columns + { + ActiveColumn = 0, + NameColumn, + PackFormatColumn, + DateColumn, + NUM_COLUMNS + }; + explicit ResourcePackFolderModel(const QString &dir); - QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + [[nodiscard]] int columnCount(const QModelIndex &parent) const override; + + [[nodiscard]] Task* createUpdateTask() override; + [[nodiscard]] Task* createParseTask(Resource&) 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..be0e752d --- /dev/null +++ b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h @@ -0,0 +1,64 @@ +#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) + { + m_create_func = [](QFileInfo const& entry) -> Resource* { + return new Resource(entry); + }; + } + BasicFolderLoadTask(QDir dir, std::function<Resource*(QFileInfo const&)> create_function) + : Task(nullptr, false), m_dir(dir), m_result(new Result), m_create_func(std::move(create_function)) + {} + + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override + { + m_aborted.store(true); + return true; + } + + void executeTask() override + { + m_dir.refresh(); + for (auto entry : m_dir.entryInfoList()) { + auto resource = m_create_func(entry); + m_result->resources.insert(resource->internal_id(), resource); + } + + if (m_aborted) + emit finished(); + else + emitSucceeded(); + } + +private: + QDir m_dir; + ResultPtr m_result; + + std::atomic<bool> m_aborted = false; + + std::function<Resource*(QFileInfo const&)> m_create_func; +}; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 1519f49d..8a6e54d8 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.store(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) + emit finished(); + else + emitSucceeded(); } diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index ed92394c..413eb2d1 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; + + std::atomic<bool> m_aborted = false; }; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp new file mode 100644 index 00000000..4f87bc13 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "LocalResourcePackParseTask.h" + +#include "FileSystem.h" +#include "Json.h" + +#include <quazip/quazip.h> +#include <quazip/quazipfile.h> + +#include <QCryptographicHash> + +namespace ResourcePackUtils { + +bool process(ResourcePack& pack) +{ + switch (pack.type()) { + case ResourceType::FOLDER: + ResourcePackUtils::processFolder(pack); + return true; + case ResourceType::ZIPFILE: + ResourcePackUtils::processZIP(pack); + return true; + default: + qWarning() << "Invalid type for resource pack parse task!"; + return false; + } +} + +void processFolder(ResourcePack& pack) +{ + Q_ASSERT(pack.type() == ResourceType::FOLDER); + + QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta")); + if (mcmeta_file_info.isFile()) { + QFile mcmeta_file(mcmeta_file_info.filePath()); + if (!mcmeta_file.open(QIODevice::ReadOnly)) + return; + + auto data = mcmeta_file.readAll(); + + ResourcePackUtils::processMCMeta(pack, std::move(data)); + + mcmeta_file.close(); + } + + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); + if (image_file_info.isFile()) { + QFile mcmeta_file(image_file_info.filePath()); + if (!mcmeta_file.open(QIODevice::ReadOnly)) + return; + + auto data = mcmeta_file.readAll(); + + ResourcePackUtils::processPackPNG(pack, std::move(data)); + + mcmeta_file.close(); + } +} + +void processZIP(ResourcePack& pack) +{ + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("pack.mcmeta")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return; + } + + auto data = file.readAll(); + + ResourcePackUtils::processMCMeta(pack, std::move(data)); + + file.close(); + } + + if (zip.setCurrentFile("pack.png")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return; + } + + auto data = file.readAll(); + + ResourcePackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + } + + zip.close(); +} + +// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta +void processMCMeta(ResourcePack& pack, QByteArray&& raw_data) +{ + try { + auto json_doc = QJsonDocument::fromJson(raw_data); + auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); + + pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); + pack.setDescription(Json::ensureString(pack_obj, "description", "")); + } catch (Json::JsonException& e) { + qWarning() << "JsonException: " << e.what() << e.cause(); + } +} + +void processPackPNG(ResourcePack& pack, QByteArray&& raw_data) +{ + auto img = QImage::fromData(raw_data); + if (!img.isNull()) { + pack.setImage(img); + } else { + qWarning() << "Failed to parse pack.png."; + } +} +} // namespace ResourcePackUtils + +LocalResourcePackParseTask::LocalResourcePackParseTask(int token, ResourcePack& rp) + : Task(nullptr, false), m_token(token), m_resource_pack(rp) +{} + +bool LocalResourcePackParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalResourcePackParseTask::executeTask() +{ + Q_ASSERT(m_resource_pack.valid()); + + if (!ResourcePackUtils::process(m_resource_pack)) + return; + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h new file mode 100644 index 00000000..d3c25464 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <QDebug> +#include <QObject> + +#include "minecraft/mod/ResourcePack.h" + +#include "tasks/Task.h" + +namespace ResourcePackUtils { +bool process(ResourcePack& pack); + +void processZIP(ResourcePack& pack); +void processFolder(ResourcePack& pack); + +void processMCMeta(ResourcePack& pack, QByteArray&& raw_data); +void processPackPNG(ResourcePack& pack, QByteArray&& raw_data); +} // namespace ResourcePackUtils + +class LocalResourcePackParseTask : public Task { + Q_OBJECT + public: + LocalResourcePackParseTask(int token, ResourcePack& rp); + + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override; + + void executeTask() override; + + [[nodiscard]] int token() const { return m_token; } + + private: + int m_token; + + ResourcePack& m_resource_pack; + + bool m_aborted = false; +}; diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index a2e055ba..3a857740 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) + : Task(nullptr, 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,23 @@ 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(); + } + } + } + + if (m_aborted) + emit finished(); + else + emitSucceeded(); } void ModFolderLoadTask::getFromMetadata() diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h index 0b6bb6cc..0f18b8b9 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,26 @@ 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); + + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override + { + m_aborted.store(true); + return true; + } + + + 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; + + std::atomic<bool> m_aborted = false; }; 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/ModAPI.h b/launcher/modplatform/ModAPI.h index 4114d83c..c7408835 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -73,7 +73,7 @@ class ModAPI { }; virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0; - virtual void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) = 0; + virtual void getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) = 0; virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob* = 0; virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0; @@ -85,7 +85,7 @@ class ModAPI { ModLoaderTypes loaders; }; - virtual void getVersions(CallerType* caller, VersionSearchArgs&& args) const = 0; + virtual void getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const = 0; static auto getModLoaderString(ModLoaderType type) -> const QString { switch (type) { 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 dc297d03..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 { @@ -73,6 +75,8 @@ struct ExtraPackData { QString sourceUrl; QString wikiUrl; QString discordUrl; + + QString body; }; struct IndexedPack { 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 f55873e9..a7124d59 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -46,10 +46,16 @@ #include "minecraft/PackProfile.h" #include "meta/Version.h" -#include <nonstd/optional> +#include <optional> 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; @@ -131,8 +139,8 @@ private: Meta::VersionPtr minecraftVersion; QMap<QString, Meta::VersionPtr> componentsToInstall; - QFuture<nonstd::optional<QStringList>> m_extractFuture; - QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; + QFuture<std::optional<QStringList>> m_extractFuture; + QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher; QFuture<bool> m_modExtractFuture; QFutureWatcher<bool> m_modExtractFutureWatcher; 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/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index c1f56658..058d2471 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -7,6 +7,13 @@ Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAcc : m_network(network), m_toProcess(toProcess) {} +bool Flame::FileResolvingTask::abort() +{ + if (m_dljob) + return m_dljob->abort(); + return true; +} + void Flame::FileResolvingTask::executeTask() { setStatus(tr("Resolving mod IDs...")); diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h index 87981f0a..f71b87ce 100644 --- a/launcher/modplatform/flame/FileResolvingTask.h +++ b/launcher/modplatform/flame/FileResolvingTask.h @@ -13,6 +13,9 @@ public: explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest &toProcess); virtual ~FileResolvingTask() {}; + bool canAbort() const override { return true; } + bool abort() override; + const Flame::Manifest &getResults() const { return m_toProcess; diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 0ff04f72..9c74918b 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -67,6 +67,43 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString return changelog; } +auto FlameAPI::getModDescription(int modId) -> QString +{ + QEventLoop lock; + QString description; + + auto* netJob = new NetJob(QString("Flame::ModDescription"), APPLICATION->network()); + auto* response = new QByteArray(); + netJob->addNetAction(Net::Download::makeByteArray( + QString("https://api.curseforge.com/v1/mods/%1/description") + .arg(QString::number(modId)), response)); + + QObject::connect(netJob, &NetJob::succeeded, [netJob, response, &description] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Flame::ModDescription at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + netJob->failed(parse_error.errorString()); + return; + } + + description = Json::ensureString(doc.object(), "data"); + }); + + QObject::connect(netJob, &NetJob::finished, [response, &lock] { + delete response; + lock.quit(); + }); + + netJob->start(); + lock.exec(); + + return description; +} + auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion { QEventLoop loop; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 336df387..4eac0664 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -7,6 +7,7 @@ class FlameAPI : public NetworkModAPI { public: auto matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr; auto getModFileChangelog(int modId, int fileId) -> QString; + auto getModDescription(int modId) -> QString; auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 746018e2..32aa4bdb 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -4,10 +4,9 @@ #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/flame/FlameAPI.h" -#include "net/NetJob.h" -static ModPlatform::ProviderCapabilities ProviderCaps; static FlameAPI api; +static ModPlatform::ProviderCapabilities ProviderCaps; void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { @@ -31,10 +30,11 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) pack.authors.append(packAuthor); } - loadExtraPackData(pack, obj); + pack.extraDataLoaded = false; + loadURLs(pack, obj); } -void FlameMod::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj) +void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj) { auto links_obj = Json::ensureObject(obj, "links"); @@ -50,7 +50,16 @@ void FlameMod::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob if(pack.extraData.wikiUrl.endsWith('/')) pack.extraData.wikiUrl.chop(1); - pack.extraDataLoaded = true; + if (!pack.extraData.body.isEmpty()) + pack.extraDataLoaded = true; +} + +void FlameMod::loadBody(ModPlatform::IndexedPack& pack, QJsonObject& obj) +{ + pack.extraData.body = api.getModDescription(pack.addonId.toInt()); + + if (!pack.extraData.issuesUrl.isEmpty() || !pack.extraData.sourceUrl.isEmpty() || !pack.extraData.wikiUrl.isEmpty()) + pack.extraDataLoaded = true; } static QString enumToString(int hash_algorithm) diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index a839dd83..db63cdbb 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -12,7 +12,8 @@ namespace FlameMod { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); -void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr<QNetworkAccessManager>& network, 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/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp index 90edfe31..866e7540 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.cpp +++ b/launcher/modplatform/helpers/NetworkModAPI.cpp @@ -31,48 +31,48 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const netJob->start(); } -void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) +void NetworkModAPI::getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) { auto response = new QByteArray(); auto job = getProject(pack.addonId.toString(), response); - QObject::connect(job, &NetJob::succeeded, caller, [caller, &pack, response] { + QObject::connect(job, &NetJob::succeeded, [callback, &pack, response] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset + qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << *response; return; } - caller->infoRequestFinished(doc, pack); + callback(doc, pack); }); job->start(); } -void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) const +void NetworkModAPI::getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const { - auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(caller->debugName()).arg(args.addonId), APPLICATION->network()); + auto netJob = new NetJob(QString("ModVersions(%2)").arg(args.addonId), APPLICATION->network()); auto response = new QByteArray(); netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response)); - QObject::connect(netJob, &NetJob::succeeded, caller, [response, caller, args] { + QObject::connect(netJob, &NetJob::succeeded, [response, callback, args] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset + qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << *response; return; } - caller->versionRequestSucceeded(doc, args.addonId); + callback(doc, args.addonId); }); - QObject::connect(netJob, &NetJob::finished, caller, [response, netJob] { + QObject::connect(netJob, &NetJob::finished, [response, netJob] { netJob->deleteLater(); delete response; }); diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h index 989bcec4..b8af22c7 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.h +++ b/launcher/modplatform/helpers/NetworkModAPI.h @@ -5,8 +5,8 @@ class NetworkModAPI : public ModAPI { public: void searchMods(CallerType* caller, SearchArgs&& args) const override; - void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) override; - void getVersions(CallerType* caller, VersionSearchArgs&& args) const override; + void getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) override; + void getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const override; auto getProject(QString addonId, QByteArray* response) const -> NetJob* override; diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h index a7395220..da4c0da5 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.h +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -10,7 +10,7 @@ #include "net/NetJob.h" -#include <nonstd/optional> +#include <optional> namespace LegacyFTB { @@ -46,8 +46,8 @@ private: /* data */ shared_qobject_ptr<QNetworkAccessManager> m_network; bool abortable = false; std::unique_ptr<QuaZip> m_packZip; - QFuture<nonstd::optional<QStringList>> m_extractFuture; - QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; + QFuture<std::optional<QStringList>> m_extractFuture; + QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher; NetJob::Ptr netJobContainer; QString archivePath; diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index cac432cd..3c15667c 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -1,7 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (C) 2022 flowln <flowlnlnln@gmail.com> * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * 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 @@ -40,103 +42,193 @@ #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "modplatform/flame/PackManifest.h" #include "net/ChecksumValidator.h" #include "settings/INISettingsObject.h" -#include "BuildConfig.h" #include "Application.h" +#include "BuildConfig.h" +#include "ui/dialogs/BlockedModsDialog.h" namespace ModpacksCH { -PackInstallTask::PackInstallTask(Modpack pack, QString version) -{ - m_pack = pack; - m_version_name = version; -} +PackInstallTask::PackInstallTask(Modpack pack, QString version, QWidget* parent) + : m_pack(std::move(pack)), m_version_name(std::move(version)), m_parent(parent) +{} bool PackInstallTask::abort() { - if(abortable) - { - return jobPtr->abort(); - } - return false; + bool aborted = true; + + if (m_net_job) + aborted &= m_net_job->abort(); + if (m_mod_id_resolver_task) + aborted &= m_mod_id_resolver_task->abort(); + + // FIXME: This should be 'emitAborted()', but InstanceStaging doesn't connect to the abort signal yet... + if (aborted) + emitFailed(tr("Aborted")); + + return aborted; } void PackInstallTask::executeTask() { - // Find pack version - bool found = false; - VersionInfo version; + setStatus(tr("Getting the manifest...")); - for(auto vInfo : m_pack.versions) { - if (vInfo.name == m_version_name) { - found = true; - version = vInfo; - break; - } - } + // Find pack version + auto version_it = std::find_if(m_pack.versions.constBegin(), m_pack.versions.constEnd(), + [this](ModpacksCH::VersionInfo const& a) { return a.name == m_version_name; }); - if(!found) { + if (version_it == m_pack.versions.constEnd()) { emitFailed(tr("Failed to find pack version %1").arg(m_version_name)); return; } - auto *netJob = new NetJob("ModpacksCH::VersionFetch", APPLICATION->network()); + auto version = *version_it; + + auto* netJob = new NetJob("ModpacksCH::VersionFetch", APPLICATION->network()); + auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1/%2").arg(m_pack.id).arg(version.id); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); - jobPtr = netJob; - jobPtr->start(); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &m_response)); + + QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onManifestDownloadSucceeded); + QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onManifestDownloadFailed); + QObject::connect(netJob, &NetJob::progress, this, &PackInstallTask::setProgress); - QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); - QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed); + m_net_job = netJob; + + netJob->start(); } -void PackInstallTask::onDownloadSucceeded() +void PackInstallTask::onManifestDownloadSucceeded() { - jobPtr.reset(); - - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << response; + m_net_job.reset(); + + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << m_response; return; } - auto obj = doc.object(); - ModpacksCH::Version version; - try - { + try { + auto obj = Json::requireObject(doc); ModpacksCH::loadVersion(version, obj); - } - catch (const JSONValidationError &e) - { + } catch (const JSONValidationError& e) { emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); return; } + m_version = version; - downloadPack(); + resolveMods(); } -void PackInstallTask::onDownloadFailed(QString reason) +void PackInstallTask::resolveMods() { - jobPtr.reset(); - emitFailed(reason); + setStatus(tr("Resolving mods...")); + setProgress(0, 100); + + m_file_id_map.clear(); + + Flame::Manifest manifest; + int index = 0; + + for (auto const& file : m_version.files) { + if (!file.serverOnly && file.url.isEmpty()) { + if (file.curseforge.file_id <= 0) { + emitFailed(tr("Invalid manifest: There's no information available to download the file '%1'!").arg(file.name)); + return; + } + + Flame::File flame_file; + flame_file.projectId = file.curseforge.project_id; + flame_file.fileId = file.curseforge.file_id; + flame_file.hash = file.sha1; + + manifest.files.insert(flame_file.fileId, flame_file); + m_file_id_map.append(flame_file.fileId); + } else { + m_file_id_map.append(-1); + } + + index++; + } + + m_mod_id_resolver_task = new Flame::FileResolvingTask(APPLICATION->network(), manifest); + + connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::succeeded, this, &PackInstallTask::onResolveModsSucceeded); + connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::failed, this, &PackInstallTask::onResolveModsFailed); + connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::progress, this, &PackInstallTask::setProgress); + + m_mod_id_resolver_task->start(); +} + +void PackInstallTask::onResolveModsSucceeded() +{ + m_abortable = false; + + QString text; + QList<QUrl> urls; + auto anyBlocked = false; + + Flame::Manifest results = m_mod_id_resolver_task->getResults(); + for (int index = 0; index < m_file_id_map.size(); index++) { + auto const file_id = m_file_id_map.at(index); + if (file_id < 0) + continue; + + Flame::File results_file = results.files[file_id]; + VersionFile& local_file = m_version.files[index]; + + // First check for blocked mods + if (!results_file.resolved || results_file.url.isEmpty()) { + QString type(local_file.type); + + 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(); + } + } + + m_mod_id_resolver_task.reset(); + + if (anyBlocked) { + qDebug() << "Blocked files found, displaying file list"; + + 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, + urls); + + if (message_dialog->exec() == QDialog::Accepted) + downloadPack(); + else + abort(); + } else { + downloadPack(); + } } void PackInstallTask::downloadPack() { setStatus(tr("Downloading mods...")); - jobPtr = new NetJob(tr("Mod download"), APPLICATION->network()); - for(auto file : m_version.files) { - if(file.serverOnly) continue; + auto* jobPtr = new NetJob(tr("Mod download"), APPLICATION->network()); + for (auto const& file : m_version.files) { + if (file.serverOnly || file.url.isEmpty()) + continue; - QFileInfo fileName(file.name); - auto cacheName = fileName.completeBaseName() + "-" + file.sha1 + "." + fileName.suffix(); + QFileInfo file_info(file.name); + auto cacheName = file_info.completeBaseName() + "-" + file.sha1 + "." + file_info.suffix(); auto entry = APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", cacheName); entry->setStale(true); @@ -144,58 +236,64 @@ void PackInstallTask::downloadPack() auto relpath = FS::PathCombine("minecraft", file.path, file.name); auto path = FS::PathCombine(m_stagingPath, relpath); - if (filesToCopy.contains(path)) { + if (m_files_to_copy.contains(path)) { qWarning() << "Ignoring" << file.url << "as a file of that path is already downloading."; continue; } + qDebug() << "Will download" << file.url << "to" << path; - filesToCopy[path] = entry->getFullPath(); + m_files_to_copy[path] = entry->getFullPath(); auto dl = Net::Download::makeCached(file.url, entry); if (!file.sha1.isEmpty()) { auto rawSha1 = QByteArray::fromHex(file.sha1.toLatin1()); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); } + jobPtr->addNetAction(dl); } - connect(jobPtr.get(), &NetJob::succeeded, this, [&]() - { - abortable = false; - jobPtr.reset(); - install(); - }); - connect(jobPtr.get(), &NetJob::failed, [&](QString reason) - { - abortable = false; - jobPtr.reset(); - emitFailed(reason); - }); - connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) - { - abortable = true; - setProgress(current, total); - }); + connect(jobPtr, &NetJob::succeeded, this, &PackInstallTask::onModDownloadSucceeded); + connect(jobPtr, &NetJob::failed, this, &PackInstallTask::onModDownloadFailed); + connect(jobPtr, &NetJob::progress, this, &PackInstallTask::setProgress); + m_net_job = jobPtr; jobPtr->start(); + + m_abortable = true; +} + +void PackInstallTask::onModDownloadSucceeded() +{ + m_net_job.reset(); + install(); } void PackInstallTask::install() { - setStatus(tr("Copying modpack files")); + setStatus(tr("Copying modpack files...")); + setProgress(0, m_files_to_copy.size()); + QCoreApplication::processEvents(); + + m_abortable = false; - for (auto iter = filesToCopy.begin(); iter != filesToCopy.end(); iter++) { - auto &to = iter.key(); - auto &from = iter.value(); + int i = 0; + for (auto iter = m_files_to_copy.constBegin(); iter != m_files_to_copy.constEnd(); iter++) { + auto& to = iter.key(); + auto& from = iter.value(); FS::copy fileCopyOperation(from, to); - if(!fileCopyOperation()) { + if (!fileCopyOperation()) { qWarning() << "Failed to copy" << from << "to" << to; emitFailed(tr("Failed to copy files")); return; } + + setProgress(i++, m_files_to_copy.size()); + QCoreApplication::processEvents(); } - setStatus(tr("Installing modpack")); + setStatus(tr("Installing modpack...")); + QCoreApplication::processEvents(); auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath); @@ -205,20 +303,20 @@ void PackInstallTask::install() auto components = instance.getPackProfile(); components->buildingFromScratch(); - for(auto target : m_version.targets) { - if(target.type == "game" && target.name == "minecraft") { + for (auto target : m_version.targets) { + if (target.type == "game" && target.name == "minecraft") { components->setComponentVersion("net.minecraft", target.version, true); break; } } - for(auto target : m_version.targets) { - if(target.type != "modloader") continue; + for (auto target : m_version.targets) { + if (target.type != "modloader") + continue; - if(target.name == "forge") { + if (target.name == "forge") { components->setComponentVersion("net.minecraftforge", target.version); - } - else if(target.name == "fabric") { + } else if (target.name == "fabric") { components->setComponentVersion("net.fabricmc.fabric-loader", target.version); } } @@ -245,4 +343,20 @@ void PackInstallTask::install() emitSucceeded(); } +void PackInstallTask::onManifestDownloadFailed(QString reason) +{ + m_net_job.reset(); + emitFailed(reason); +} +void PackInstallTask::onResolveModsFailed(QString reason) +{ + m_net_job.reset(); + emitFailed(reason); } +void PackInstallTask::onModDownloadFailed(QString reason) +{ + m_net_job.reset(); + emitFailed(reason); +} + +} // namespace ModpacksCH diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.h b/launcher/modplatform/modpacksch/FTBPackInstallTask.h index ff59b695..e63ca0df 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.h +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.h @@ -1,18 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com> + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <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> + * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -20,44 +40,60 @@ #include "FTBPackManifest.h" #include "InstanceTask.h" +#include "QObjectPtr.h" +#include "modplatform/flame/FileResolvingTask.h" #include "net/NetJob.h" +#include <QWidget> + namespace ModpacksCH { -class PackInstallTask : public InstanceTask +class PackInstallTask final : public InstanceTask { Q_OBJECT public: - explicit PackInstallTask(Modpack pack, QString version); - virtual ~PackInstallTask(){} + explicit PackInstallTask(Modpack pack, QString version, QWidget* parent = nullptr); + ~PackInstallTask() override = default; - bool canAbort() const override { return true; } + bool canAbort() const override { return m_abortable; } bool abort() override; protected: - virtual void executeTask() override; + void executeTask() override; private slots: - void onDownloadSucceeded(); - void onDownloadFailed(QString reason); + void onManifestDownloadSucceeded(); + void onResolveModsSucceeded(); + void onModDownloadSucceeded(); + + void onManifestDownloadFailed(QString reason); + void onResolveModsFailed(QString reason); + void onModDownloadFailed(QString reason); private: + void resolveMods(); void downloadPack(); void install(); private: - bool abortable = false; + bool m_abortable = true; + + NetJob::Ptr m_net_job = nullptr; + shared_qobject_ptr<Flame::FileResolvingTask> m_mod_id_resolver_task = nullptr; + + QList<int> m_file_id_map; - NetJob::Ptr jobPtr; - QByteArray response; + QByteArray m_response; Modpack m_pack; QString m_version_name; Version m_version; - QMap<QString, QString> filesToCopy; + QMap<QString, QString> m_files_to_copy; + //FIXME: nuke + QWidget* m_parent; }; } diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.cpp b/launcher/modplatform/modpacksch/FTBPackManifest.cpp index e2d47a5b..421527ae 100644 --- a/launcher/modplatform/modpacksch/FTBPackManifest.cpp +++ b/launcher/modplatform/modpacksch/FTBPackManifest.cpp @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com> + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "FTBPackManifest.h" @@ -127,13 +146,16 @@ static void loadVersionFile(ModpacksCH::VersionFile & a, QJsonObject & obj) a.path = Json::requireString(obj, "path"); a.name = Json::requireString(obj, "name"); a.version = Json::requireString(obj, "version"); - a.url = Json::requireString(obj, "url"); + a.url = Json::ensureString(obj, "url"); // optional a.sha1 = Json::requireString(obj, "sha1"); a.size = Json::requireInteger(obj, "size"); a.clientOnly = Json::requireBoolean(obj, "clientonly"); a.serverOnly = Json::requireBoolean(obj, "serveronly"); a.optional = Json::requireBoolean(obj, "optional"); a.updated = Json::requireInteger(obj, "updated"); + auto curseforgeObj = Json::ensureObject(obj, "curseforge"); // optional + a.curseforge.project_id = Json::ensureInteger(curseforgeObj, "project"); + a.curseforge.file_id = Json::ensureInteger(curseforgeObj, "file"); } void ModpacksCH::loadVersion(ModpacksCH::Version & m, QJsonObject & obj) diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.h b/launcher/modplatform/modpacksch/FTBPackManifest.h index da45d8ac..a8b6f35e 100644 --- a/launcher/modplatform/modpacksch/FTBPackManifest.h +++ b/launcher/modplatform/modpacksch/FTBPackManifest.h @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org> - * Copyright 2020 Petr Mrazek <peterix@gmail.com> + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <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> + * Copyright 2020 Petr Mrazek <peterix@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 @@ -97,6 +116,12 @@ struct VersionTarget int64_t updated; }; +struct VersionFileCurseForge +{ + int project_id; + int file_id; +}; + struct VersionFile { int id; @@ -111,6 +136,7 @@ struct VersionFile bool serverOnly; bool optional; int64_t updated; + VersionFileCurseForge curseforge; }; struct Version 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/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index e50dd96d..3e53becb 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -87,6 +87,8 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob pack.extraData.donate.append(donate); } + pack.extraData.body = Json::ensureString(obj, "body"); + pack.extraDataLoaded = true; } diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.h b/launcher/modplatform/technic/SingleZipPackInstallTask.h index 4d1fcbff..981ccf8a 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.h +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.h @@ -24,7 +24,7 @@ #include <QStringList> #include <QUrl> -#include <nonstd/optional> +#include <optional> namespace Technic { @@ -57,8 +57,8 @@ private: QString m_archivePath; NetJob::Ptr m_filesNetJob; std::unique_ptr<QuaZip> m_packZip; - QFuture<nonstd::optional<QStringList>> m_extractFuture; - QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; + QFuture<std::optional<QStringList>> m_extractFuture; + QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher; }; } // namespace Technic 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..8ced1b7e 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -35,204 +35,95 @@ */ #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); + if (fullyAborted) + emitAborted(); + else + emitFailed(tr("Failed to abort all tasks in the NetJob!")); - m_doing.remove(index); - m_done.insert(index); - m_downloads[index].get()->disconnect(this); - - startMoreParts(); + return fullyAborted; } -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..ce08a6a2 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,40 @@ void ConcurrentTask::executeTask() { m_total_size = m_queue.size(); - for (int i = 0; i < m_total_max_size; i++) - startNext(); + // Start the least amount of tasks needed, but at least one + int num_starts = std::max(1, 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 +78,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 +101,8 @@ void ConcurrentTask::startNext() setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus()); updateState(); + QCoreApplication::processEvents(); + next->start(); } @@ -127,11 +139,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 deleted file mode 100644 index ef153a6a..00000000 --- a/launcher/tasks/Task_test.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include <QTest> - -#include "Task.h" - -/* Does nothing. Only used for testing. */ -class BasicTask : public Task { - Q_OBJECT - - friend class TaskTest; - - private: - void executeTask() override {}; -}; - -/* Does nothing. Only used for testing. */ -class BasicTask_MultiStep : public Task { - Q_OBJECT - - friend class TaskTest; - - private: - auto isMultiStep() const -> bool override { return true; } - - void executeTask() override {}; -}; - -class TaskTest : public QObject { - Q_OBJECT - - private slots: - void test_SetStatus_NoMultiStep(){ - BasicTask t; - QString status {"test status"}; - - t.setStatus(status); - - QCOMPARE(t.getStatus(), status); - QCOMPARE(t.getStepStatus(), status); - } - - void test_SetStatus_MultiStep(){ - BasicTask_MultiStep t; - QString status {"test status"}; - - t.setStatus(status); - - QCOMPARE(t.getStatus(), status); - // Even though it is multi step, it does not override the getStepStatus method, - // so it should remain the same. - QCOMPARE(t.getStepStatus(), status); - } - - void test_SetProgress(){ - BasicTask t; - int current = 42; - int total = 207; - - t.setProgress(current, total); - - QCOMPARE(t.getProgress(), current); - QCOMPARE(t.getTotalProgress(), total); - } -}; - -QTEST_GUILESS_MAIN(TaskTest) - -#include "Task_test.moc" diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index d58f158e..299401f5 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -252,6 +252,9 @@ public: TranslatedAction actionViewInstanceFolder; TranslatedAction actionViewCentralModsFolder; + QMenu * editMenu = nullptr; + TranslatedAction actionUndoTrashInstance; + QMenu * helpMenu = nullptr; TranslatedToolButton helpMenuButton; TranslatedAction actionReportBug; @@ -335,6 +338,14 @@ public: actionSettings->setShortcut(QKeySequence::Preferences); all_actions.append(&actionSettings); + actionUndoTrashInstance = TranslatedAction(MainWindow); + connect(actionUndoTrashInstance, SIGNAL(triggered(bool)), MainWindow, SLOT(undoTrashInstance())); + actionUndoTrashInstance->setObjectName(QStringLiteral("actionUndoTrashInstance")); + actionUndoTrashInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Undo Last Instance Deletion")); + actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); + actionUndoTrashInstance->setShortcut(QKeySequence("Ctrl+Z")); + all_actions.append(&actionUndoTrashInstance); + if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { actionReportBug = TranslatedAction(MainWindow); actionReportBug->setObjectName(QStringLiteral("actionReportBug")); @@ -508,6 +519,9 @@ public: fileMenu->addSeparator(); fileMenu->addAction(actionSettings); + editMenu = menuBar->addMenu(tr("&Edit")); + editMenu->addAction(actionUndoTrashInstance); + viewMenu = menuBar->addMenu(tr("&View")); viewMenu->setSeparatorsCollapsible(false); viewMenu->addAction(actionCAT); @@ -732,9 +746,10 @@ public: actionDeleteInstance = TranslatedAction(MainWindow); actionDeleteInstance->setObjectName(QStringLiteral("actionDeleteInstance")); - actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Dele&te Instance...")); + actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Dele&te Instance")); actionDeleteInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Delete the selected instance.")); actionDeleteInstance->setShortcuts({QKeySequence(tr("Backspace")), QKeySequence::Delete}); + actionDeleteInstance->setAutoRepeat(false); all_actions.append(&actionDeleteInstance); actionCopyInstance = TranslatedAction(MainWindow); @@ -1150,6 +1165,11 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos) connect(actionDeleteGroup, SIGNAL(triggered(bool)), SLOT(deleteGroup())); actions.append(actionDeleteGroup); } + + QAction *actionUndoTrashInstance = new QAction("Undo last trash instance", this); + connect(actionUndoTrashInstance, SIGNAL(triggered(bool)), SLOT(undoTrashInstance())); + actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); + actions.append(actionUndoTrashInstance); } QMenu myMenu; myMenu.addActions(actions); @@ -1445,6 +1465,7 @@ void MainWindow::updateNewsLabel() { newsLabel->setText(tr("Loading news...")); newsLabel->setEnabled(false); + ui->actionMoreNews->setVisible(false); } else { @@ -1453,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); } } } @@ -1832,6 +1855,11 @@ void MainWindow::deleteGroup() } } +void MainWindow::undoTrashInstance() +{ + APPLICATION->instances()->undoTrashInstance(); +} + void MainWindow::on_actionViewInstanceFolder_triggered() { QString str = APPLICATION->settings()->get("InstanceDir").toString(); @@ -1957,7 +1985,12 @@ void MainWindow::on_actionDeleteInstance_triggered() { return; } + auto id = m_selectedInstance->id(); + if (APPLICATION->instances()->trashInstance(id)) { + return; + } + auto response = CustomMessageBox::selectable( this, tr("CAREFUL!"), diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index d7930b5a..dde3d02c 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -145,6 +145,7 @@ private slots: void on_actionDeleteInstance_triggered(); void deleteGroup(); + void undoTrashInstance(); void on_actionExportInstance_triggered(); 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 e4fc3ecc..d740c8cb 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -19,36 +19,33 @@ #include "ModDownloadDialog.h" #include <BaseVersion.h> -#include <icons/IconList.h> #include <InstanceList.h> +#include <icons/IconList.h> #include "Application.h" -#include "ProgressDialog.h" #include "ReviewMessageBox.h" +#include <QDialogButtonBox> #include <QLayout> #include <QPushButton> #include <QValidator> -#include <QDialogButtonBox> -#include "ui/widgets/PageContainer.h" -#include "ui/pages/modplatform/modrinth/ModrinthModPage.h" #include "ModDownloadTask.h" +#include "ui/pages/modplatform/flame/FlameModPage.h" +#include "ui/pages/modplatform/modrinth/ModrinthModPage.h" +#include "ui/widgets/PageContainer.h" - -ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods, QWidget *parent, - BaseInstance *instance) - : QDialog(parent), mods(mods), m_instance(instance) +ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance) + : QDialog(parent), mods(mods), m_verticalLayout(new QVBoxLayout(this)), m_instance(instance) { setObjectName(QStringLiteral("ModDownloadDialog")); - - resize(std::max(0.5*parent->width(), 400.0), std::max(0.75*parent->height(), 400.0)); - - m_verticalLayout = new QVBoxLayout(this); m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0)); + setWindowIcon(APPLICATION->getThemedIcon("new")); - // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not move this below. + // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not + // move this below. m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel); m_container = new PageContainer(this); @@ -58,12 +55,17 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods m_container->addButtons(m_buttons); + connect(m_container, &PageContainer::selectedPageChanged, this, &ModDownloadDialog::selectedPageChanged); + // Bonk Qt over its stupid head and make sure it understands which button is the default one... // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button auto OkButton = m_buttons->button(QDialogButtonBox::Ok); OkButton->setEnabled(false); OkButton->setDefault(true); OkButton->setAutoDefault(true); + OkButton->setText(tr("Review and confirm")); + OkButton->setShortcut(tr("Ctrl+Return")); + OkButton->setToolTip(tr("Opens a new popup to review your selected mods and confirm your selection. Shortcut: Ctrl+Return")); connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::confirm); auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); @@ -78,7 +80,9 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods QMetaObject::connectSlotsByName(this); setWindowModality(Qt::WindowModal); - setWindowTitle("Download mods"); + setWindowTitle(dialogTitle()); + + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("ModDownloadGeometry").toByteArray())); } QString ModDownloadDialog::dialogTitle() @@ -88,6 +92,7 @@ QString ModDownloadDialog::dialogTitle() void ModDownloadDialog::reject() { + APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64()); QDialog::reject(); } @@ -114,21 +119,22 @@ void ModDownloadDialog::confirm() void ModDownloadDialog::accept() { + APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64()); QDialog::accept(); } -QList<BasePage *> ModDownloadDialog::getPages() +QList<BasePage*> ModDownloadDialog::getPages() { - QList<BasePage *> pages; + QList<BasePage*> pages; - pages.append(new ModrinthModPage(this, m_instance)); - if (APPLICATION->currentCapabilities() & Application::SupportsFlame) - pages.append(new FlameModPage(this, m_instance)); + pages.append(ModrinthModPage::create(this, m_instance)); + if (APPLICATION->capabilities() & Application::SupportsFlame) + pages.append(FlameModPage::create(this, m_instance)); return pages; } -void ModDownloadDialog::addSelectedMod(const QString& name, ModDownloadTask* task) +void ModDownloadDialog::addSelectedMod(QString name, ModDownloadTask* task) { removeSelectedMod(name); modTask.insert(name, task); @@ -136,16 +142,16 @@ void ModDownloadDialog::addSelectedMod(const QString& name, ModDownloadTask* tas m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty()); } -void ModDownloadDialog::removeSelectedMod(const QString &name) +void ModDownloadDialog::removeSelectedMod(QString name) { - if(modTask.contains(name)) + if (modTask.contains(name)) delete modTask.find(name).value(); modTask.remove(name); m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty()); } -bool ModDownloadDialog::isModSelected(const QString &name, const QString& filename) const +bool ModDownloadDialog::isModSelected(QString name, QString filename) const { // FIXME: Is there a way to check for versions without checking the filename // as a heuristic, other than adding such info to ModDownloadTask itself? @@ -153,16 +159,31 @@ bool ModDownloadDialog::isModSelected(const QString &name, const QString& filena return iter != modTask.end() && (iter.value()->getFilename() == filename); } -bool ModDownloadDialog::isModSelected(const QString &name) const +bool ModDownloadDialog::isModSelected(QString name) const { auto iter = modTask.find(name); return iter != modTask.end(); } -ModDownloadDialog::~ModDownloadDialog() +const QList<ModDownloadTask*> ModDownloadDialog::getTasks() { + return modTask.values(); } -const QList<ModDownloadTask*> ModDownloadDialog::getTasks() { - return modTask.values(); +void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected) +{ + auto* prev_page = dynamic_cast<ModPage*>(previous); + if (!prev_page) { + qCritical() << "Page '" << previous->displayName() << "' in ModDownloadDialog is not a ModPage!"; + return; + } + + auto* selected_page = dynamic_cast<ModPage*>(selected); + if (!selected_page) { + qCritical() << "Page '" << selected->displayName() << "' in ModDownloadDialog is not a ModPage!"; + return; + } + + // Same effect as having a global search bar + selected_page->setSearchTerm(prev_page->getSearchTerm()); } diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 1fa1f058..18a5f0f3 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -21,11 +21,9 @@ #include <QDialog> #include <QVBoxLayout> -#include "BaseVersion.h" -#include "ui/pages/BasePageProvider.h" -#include "minecraft/mod/ModFolderModel.h" #include "ModDownloadTask.h" -#include "ui/pages/modplatform/flame/FlameModPage.h" +#include "minecraft/mod/ModFolderModel.h" +#include "ui/pages/BasePageProvider.h" namespace Ui { @@ -36,21 +34,21 @@ class PageContainer; class QDialogButtonBox; class ModrinthModPage; -class ModDownloadDialog : public QDialog, public BasePageProvider +class ModDownloadDialog final : public QDialog, public BasePageProvider { Q_OBJECT public: - explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods, QWidget *parent, BaseInstance *instance); - ~ModDownloadDialog(); + explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance); + ~ModDownloadDialog() override = default; QString dialogTitle() override; - QList<BasePage *> getPages() override; + QList<BasePage*> getPages() override; - void addSelectedMod(const QString & name = QString(), ModDownloadTask * task = nullptr); - void removeSelectedMod(const QString & name = QString()); - bool isModSelected(const QString & name, const QString & filename) const; - bool isModSelected(const QString & name) const; + void addSelectedMod(QString name = QString(), ModDownloadTask* task = nullptr); + void removeSelectedMod(QString name = QString()); + bool isModSelected(QString name, QString filename) const; + bool isModSelected(QString name) const; const QList<ModDownloadTask*> getTasks(); const std::shared_ptr<ModFolderModel> &mods; @@ -60,6 +58,9 @@ public slots: void accept() override; void reject() override; +private slots: + void selectedPageChanged(BasePage* previous, BasePage* selected); + private: Ui::ModDownloadDialog *ui = nullptr; PageContainer * m_container = nullptr; 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/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index a79bc837..3c7f53d3 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -43,8 +43,8 @@ void ProgressDialog::setSkipButton(bool present, QString label) void ProgressDialog::on_skipButton_clicked(bool checked) { Q_UNUSED(checked); - task->abort(); - QDialog::reject(); + if (task->abort()) + QDialog::reject(); } ProgressDialog::~ProgressDialog() diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index e664e566..7c25c91c 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -1,11 +1,16 @@ #include "ReviewMessageBox.h" #include "ui_ReviewMessageBox.h" +#include <QPushButton> + ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QString const& icon) : QDialog(parent), ui(new Ui::ReviewMessageBox) { ui->setupUi(this); + auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel); + back_button->setText(tr("Back")); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject); } 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..28a874c2 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,65 @@ 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(); + return true; +} - 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; +void ModFolderPage::removeItem() +{ + + if (!m_controlsEnabled) + return; + + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + m_model->deleteMods(selection.indexes()); } void ModFolderPage::installMods() @@ -232,3 +248,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..c9a55bde 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -53,15 +53,30 @@ 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 removeItem() override; + 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..9633e3b4 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); } @@ -59,4 +61,15 @@ public: return !m_instance->traits().contains("no-texturepacks") && !m_instance->traits().contains("texturepacks"); } + + public slots: + bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override + { + auto sourceCurrent = m_filterModel->mapToSource(current); + int row = sourceCurrent.row(); + auto& rp = static_cast<ResourcePack&>(m_model->at(row)); + ui->frame->updateWithResourcePack(rp); + + return true; + } }; 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 ¤t, 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 ¤t, 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 94b1f099..029e2be0 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -2,15 +2,27 @@ #include "BuildConfig.h" #include "Json.h" +#include "ModPage.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" +#include "ui/widgets/ProjectItem.h" + #include <QMessageBox> namespace ModPlatform { -ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) {} +// HACK: We need this to prevent callbacks from calling the ListModel after it has already been deleted. +// This leaks a tiny bit of memory per time the user has opened the mod dialog. How to make this better? +static QHash<ListModel*, bool> s_running; + +ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) { s_running.insert(this, true); } + +ListModel::~ListModel() +{ + s_running.find(this).value() = false; +} auto ListModel::debugName() const -> QString { @@ -39,9 +51,6 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant ModPlatform::IndexedPack pack = modpacks.at(pos); switch (role) { - case Qt::DisplayRole: { - return pack.name; - } case Qt::ToolTipRole: { if (pack.description.length() > 100) { // some magic to prevent to long tooltips and replace html linebreaks @@ -64,20 +73,20 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); return icon; } + case Qt::SizeHintRole: + return QSize(0, 58); case Qt::UserRole: { QVariant v; v.setValue(pack); return v; } - case Qt::FontRole: { - QFont font; - if (m_parent->getDialog()->isModSelected(pack.name)) { - font.setBold(true); - font.setUnderline(true); - } - - return font; - } + // Custom data + case UserDataTypes::TITLE: + return pack.name; + case UserDataTypes::DESCRIPTION: + return pack.description; + case UserDataTypes::SELECTED: + return m_parent->getDialog()->isModSelected(pack.name); default: break; } @@ -85,11 +94,27 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant return {}; } -void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) +bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + int pos = index.row(); + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) + return false; + + modpacks[pos] = value.value<ModPlatform::IndexedPack>(); + + return true; +} + +void ListModel::requestModVersions(ModPlatform::IndexedPack const& current, QModelIndex index) { auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile(); - m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoaders() }); + m_parent->apiProvider()->getVersions({ current.addonId.toString(), getMineVersions(), profile->getModLoaders() }, + [this, current, index](QJsonDocument& doc, QString addonId) { + if (!s_running.constFind(this).value()) + return; + versionRequestSucceeded(doc, addonId, index); + }); } void ListModel::performPaginatedSearch() @@ -100,9 +125,13 @@ void ListModel::performPaginatedSearch() this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() }); } -void ListModel::requestModInfo(ModPlatform::IndexedPack& current) +void ListModel::requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index) { - m_parent->apiProvider()->getModInfo(this, current); + m_parent->apiProvider()->getModInfo(current, [this, index](QJsonDocument& doc, ModPlatform::IndexedPack& pack) { + if (!s_running.constFind(this).value()) + return; + infoRequestFinished(doc, pack, index); + }); } void ListModel::refresh() @@ -230,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 @@ -255,7 +285,7 @@ void ListModel::searchRequestFailed(QString reason) } } -void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack) +void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) { qDebug() << "Loading mod info"; @@ -267,10 +297,20 @@ void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause(); } + // Check if the index is still valid for this mod or not + if (pack.addonId == data(index, Qt::UserRole).value<ModPlatform::IndexedPack>().addonId) { + // Cache info :^) + QVariant new_pack; + new_pack.setValue(pack); + if (!setData(index, new_pack, Qt::UserRole)) { + qWarning() << "Failed to cache mod info!"; + } + } + m_parent->updateUi(); } -void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) +void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index) { auto& current = m_parent->getCurrent(); if (addonId != current.addonId) { @@ -286,6 +326,14 @@ void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause(); } + // Cache info :^) + QVariant new_pack; + new_pack.setValue(current); + if (!setData(index, new_pack, Qt::UserRole)) { + qWarning() << "Failed to cache mod versions!"; + } + + m_parent->updateModVersions(); } diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index dd22407c..a58c7c55 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -2,7 +2,6 @@ #include <QAbstractListModel> -#include "modplatform/ModAPI.h" #include "modplatform/ModIndex.h" #include "net/NetJob.h" @@ -19,7 +18,7 @@ class ListModel : public QAbstractListModel { public: ListModel(ModPage* parent); - ~ListModel() override = default; + ~ListModel() override; inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); }; inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; }; @@ -29,15 +28,17 @@ class ListModel : public QAbstractListModel { /* Retrieve information from the model at a given index with the given role */ auto data(const QModelIndex& index, int role) const -> QVariant override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } + inline NetJob* activeJob() { return jobPtr.get(); } /* Ask the API for more information */ void fetchMore(const QModelIndex& parent) override; void refresh(); void searchWithTerm(const QString& term, const int sort, const bool filter_changed); - void requestModInfo(ModPlatform::IndexedPack& current); - void requestModVersions(const ModPlatform::IndexedPack& current); + void requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index); + void requestModVersions(const ModPlatform::IndexedPack& current, QModelIndex index); virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) {}; @@ -51,9 +52,9 @@ class ListModel : public QAbstractListModel { void searchRequestFinished(QJsonDocument& doc); void searchRequestFailed(QString reason); - void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack); + void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index); - void versionRequestSucceeded(QJsonDocument doc, QString addonId); + void versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index); protected slots: diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 200fe59e..986caa77 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -40,37 +40,45 @@ #include <QKeyEvent> #include <memory> +#include <HoeDown.h> + #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" +#include "ui/widgets/ProjectItem.h" + ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) : QWidget(dialog) , m_instance(instance) , ui(new Ui::ModPage) , dialog(dialog) - , filter_widget(static_cast<MinecraftInstance*>(instance)->getPackProfile()->getComponentVersion("net.minecraft"), this) + , m_fetch_progress(this, false) , api(api) { ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); + + m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); + m_search_timer.setSingleShot(true); + + connect(&m_search_timer, &QTimer::timeout, this, &ModPage::triggerSearch); + ui->searchEdit->installEventFilter(this); ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); - ui->gridLayout_3->addWidget(&filter_widget, 0, 0, 1, ui->gridLayout_3->columnCount()); + m_fetch_progress.hideIfInactive(true); + m_fetch_progress.setFixedHeight(24); + m_fetch_progress.progressFormat(""); - filter_widget.setInstance(static_cast<MinecraftInstance*>(m_instance)); - m_filter = filter_widget.getFilter(); + ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, ui->gridLayout_3->columnCount()); - connect(&filter_widget, &ModFilterWidget::filterChanged, this, [&]{ - ui->searchButton->setStyleSheet("text-decoration: underline"); - }); - connect(&filter_widget, &ModFilterWidget::filterUnchanged, this, [&]{ - ui->searchButton->setStyleSheet("text-decoration: none"); - }); + ui->packView->setItemDelegate(new ProjectItemDelegate(this)); + ui->packView->installEventFilter(this); } ModPage::~ModPage() @@ -78,6 +86,26 @@ ModPage::~ModPage() delete ui; } +void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget) +{ + if (m_filter_widget) + disconnect(m_filter_widget.get(), nullptr, nullptr, nullptr); + + m_filter_widget.swap(widget); + + ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, ui->gridLayout_3->columnCount()); + + m_filter_widget->setInstance(static_cast<MinecraftInstance*>(m_instance)); + m_filter = m_filter_widget->getFilter(); + + connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, [&]{ + ui->searchButton->setStyleSheet("text-decoration: underline"); + }); + connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this, [&]{ + ui->searchButton->setStyleSheet("text-decoration: none"); + }); +} + /******** Qt things ********/ @@ -95,6 +123,23 @@ auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool triggerSearch(); keyEvent->accept(); return true; + } else { + if (m_search_timer.isActive()) + m_search_timer.stop(); + + m_search_timer.start(350); + } + } else if (watched == ui->packView && event->type() == QEvent::KeyPress) { + auto* keyEvent = dynamic_cast<QKeyEvent*>(event); + if (keyEvent->key() == Qt::Key_Return) { + onModSelected(); + + // To have the 'select mod' button outlined instead of the 'review and confirm' one + ui->modSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason); + ui->packView->setFocus(Qt::FocusReason::NoFocusReason); + + keyEvent->accept(); + return true; } } return QWidget::eventFilter(watched, event); @@ -105,13 +150,13 @@ auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool void ModPage::filterMods() { - filter_widget.setHidden(!filter_widget.isHidden()); + m_filter_widget->setHidden(!m_filter_widget->isHidden()); } void ModPage::triggerSearch() { - auto changed = filter_widget.changed(); - m_filter = filter_widget.getFilter(); + auto changed = m_filter_widget->changed(); + m_filter = m_filter_widget->getFilter(); if(changed){ ui->packView->clearSelection(); @@ -120,16 +165,26 @@ void ModPage::triggerSearch() updateSelectionButton(); } - listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), changed); + listModel->searchWithTerm(getSearchTerm(), ui->sortByBox->currentIndex(), changed); + m_fetch_progress.watch(listModel->activeJob()); } -void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) +QString ModPage::getSearchTerm() const +{ + return ui->searchEdit->text(); +} +void ModPage::setSearchTerm(QString term) +{ + ui->searchEdit->setText(term); +} + +void ModPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) { ui->versionSelectionBox->clear(); - if (!first.isValid()) { return; } + if (!curr.isValid()) { return; } - current = listModel->data(first, Qt::UserRole).value<ModPlatform::IndexedPack>(); + current = listModel->data(curr, Qt::UserRole).value<ModPlatform::IndexedPack>(); if (!current.versionsLoaded) { qDebug() << QString("Loading %1 mod versions").arg(debugName()); @@ -137,7 +192,7 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) ui->modSelectionButton->setText(tr("Loading versions...")); ui->modSelectionButton->setEnabled(false); - listModel->requestModVersions(current); + listModel->requestModVersions(current, curr); } else { for (int i = 0; i < current.versions.size(); i++) { ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i)); @@ -149,7 +204,8 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) if(!current.extraDataLoaded){ qDebug() << QString("Loading %1 mod info").arg(debugName()); - listModel->requestModInfo(current); + + listModel->requestModInfo(current, curr); } updateUi(); @@ -167,6 +223,9 @@ void ModPage::onVersionSelectionChanged(QString data) void ModPage::onModSelected() { + if (selectedVersion < 0) + return; + auto& version = current.versions[selectedVersion]; if (dialog->isModSelected(current.name, version.fileName)) { dialog->removeSelectedMod(current.name); @@ -176,6 +235,9 @@ void ModPage::onModSelected() } updateSelectionButton(); + + /* Force redraw on the mods list when the selection changes */ + ui->packView->adjustSize(); } @@ -285,5 +347,6 @@ void ModPage::updateUi() text += "<hr>"; - ui->packDescription->setHtml(text + current.description); + HoeDown h; + ui->packDescription->setHtml(text + (current.extraData.body.isEmpty() ? current.description : h.process(current.extraData.body.toUtf8()))); } diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index cf00e16e..3f31651c 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -8,6 +8,7 @@ #include "ui/pages/BasePage.h" #include "ui/pages/modplatform/ModModel.h" #include "ui/widgets/ModFilterWidget.h" +#include "ui/widgets/ProgressWidget.h" class ModDownloadDialog; @@ -20,7 +21,17 @@ class ModPage : public QWidget, public BasePage { Q_OBJECT public: - explicit ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api); + template<typename T> + static T* create(ModDownloadDialog* dialog, BaseInstance* instance) + { + auto page = new T(dialog, instance); + + auto filter_widget = ModFilterWidget::create(static_cast<MinecraftInstance*>(instance)->getPackProfile()->getComponentVersion("net.minecraft"), page); + page->setFilterWidget(filter_widget); + + return page; + } + ~ModPage() override; /* Affects what the user sees */ @@ -45,6 +56,13 @@ class ModPage : public QWidget, public BasePage { auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; } auto getDialog() const -> const ModDownloadDialog* { return dialog; } + /** Get the current term in the search bar. */ + auto getSearchTerm() const -> QString; + /** Programatically set the term in the search bar. */ + void setSearchTerm(QString); + + void setFilterWidget(unique_qobject_ptr<ModFilterWidget>&); + auto getCurrent() -> ModPlatform::IndexedPack& { return current; } void updateModVersions(int prev_count = -1); @@ -54,6 +72,7 @@ class ModPage : public QWidget, public BasePage { BaseInstance* m_instance; protected: + ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api); void updateSelectionButton(); protected slots: @@ -67,13 +86,18 @@ class ModPage : public QWidget, public BasePage { Ui::ModPage* ui = nullptr; ModDownloadDialog* dialog = nullptr; - ModFilterWidget filter_widget; + unique_qobject_ptr<ModFilterWidget> m_filter_widget; std::shared_ptr<ModFilterWidget::Filter> m_filter; + ProgressWidget m_fetch_progress; + ModPlatform::ListModel* listModel = nullptr; ModPlatform::IndexedPack current; std::unique_ptr<ModAPI> api; int selectedVersion = -1; + + // Used to do instant searching with a delay to cache quick changes + QTimer m_search_timer; }; 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/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index 8de2e545..bc2c686c 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -12,6 +12,12 @@ void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) FlameMod::loadIndexedPack(m, obj); } +// We already deal with the URLs when initializing the pack, due to the API response's structure +void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadBody(m, obj); +} + void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h index 707c1bb1..6a6aef2e 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h @@ -13,6 +13,7 @@ class ListModel : public ModPlatform::ListModel { private: void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 445d0368..2cd484cb 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -44,7 +44,12 @@ class FlameModPage : public ModPage { Q_OBJECT public: - explicit FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance); + static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance* instance) + { + return ModPage::create<FlameModPage>(dialog, instance); + } + + FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance); ~FlameModPage() override = default; inline auto displayName() const -> QString override { return "CurseForge"; } diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp index 8a93bc2e..504d7f7b 100644 --- a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * 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 @@ -126,7 +127,7 @@ void FtbPage::suggestCurrent() return; } - dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion)); + dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion, this)); for(auto art : selected.art) { if(art.type == "square") { QString editedLogoName; 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/ModrinthModPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h index 94985f63..40d82e6f 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h @@ -44,7 +44,12 @@ class ModrinthModPage : public ModPage { Q_OBJECT public: - explicit ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance); + static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance* instance) + { + return ModPage::create<ModrinthModPage>(dialog, instance); + } + + ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance); ~ModrinthModPage() override = default; inline auto displayName() const -> QString override { return "Modrinth"; } 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/Common.cpp b/launcher/ui/widgets/Common.cpp index f72f3596..097bb6d4 100644 --- a/launcher/ui/widgets/Common.cpp +++ b/launcher/ui/widgets/Common.cpp @@ -1,27 +1,33 @@ #include "Common.h" // Origin: Qt -QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height, - qreal &widthUsed) +// More specifically, this is a trimmed down version on the algorithm in: +// https://code.woboq.org/qt5/qtbase/src/widgets/styles/qcommonstyle.cpp.html#846 +QList<std::pair<qreal, QString>> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height) { - QStringList lines; + QList<std::pair<qreal, QString>> lines; height = 0; - widthUsed = 0; + textLayout.beginLayout(); + QString str = textLayout.text(); - while (true) - { + while (true) { QTextLine line = textLayout.createLine(); + if (!line.isValid()) break; if (line.textLength() == 0) break; + line.setLineWidth(lineWidth); line.setPosition(QPointF(0, height)); + height += line.height(); - lines.append(str.mid(line.textStart(), line.textLength())); - widthUsed = qMax(widthUsed, line.naturalTextWidth()); + + lines.append(std::make_pair(line.naturalTextWidth(), str.mid(line.textStart(), line.textLength()))); } + textLayout.endLayout(); + return lines; } diff --git a/launcher/ui/widgets/Common.h b/launcher/ui/widgets/Common.h index b3fbe1a0..b3dd5ca8 100644 --- a/launcher/ui/widgets/Common.h +++ b/launcher/ui/widgets/Common.h @@ -1,6 +1,9 @@ #pragma once -#include <QStringList> + #include <QTextLayout> -QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height, - qreal &widthUsed);
\ No newline at end of file +/** Cuts out the text in textLayout into smaller pieces, according to the lineWidth. + * Returns a list of pairs, each containing the width of that line and that line's string, respectively. + * The total height of those lines is set in the last argument, 'height'. + */ +QList<std::pair<qreal, QString>> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height); diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp new file mode 100644 index 00000000..9e0553f8 --- /dev/null +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* +* PolyMC - Minecraft Launcher +* Copyright (c) 2022 flowln <flowlnlnln@gmail.com> +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, version 3. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see <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 <QMessageBox> + +#include "InfoFrame.h" +#include "ui_InfoFrame.h" + +#include "ui/dialogs/CustomMessageBox.h" + +InfoFrame::InfoFrame(QWidget *parent) : + QFrame(parent), + ui(new Ui::InfoFrame) +{ + 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; + } + + QString text = ""; + QString name = ""; + if (m.name().isEmpty()) + name = m.internal_id(); + else + name = m.name(); + + if (m.homeurl().isEmpty()) + text = name; + else + text = "<a href=\"" + m.homeurl() + "\">" + name + "</a>"; + if (!m.authors().isEmpty()) + text += " by " + m.authors().join(", "); + + setName(text); + + if (m.description().isEmpty()) + { + setDescription(QString()); + } + else + { + setDescription(m.description()); + } + + setImage(); +} + +void InfoFrame::updateWithResource(const Resource& resource) +{ + setName(resource.name()); + setImage(); +} + +// https://www.sportskeeda.com/minecraft-wiki/color-codes +static const QMap<QChar, QString> s_value_to_color = { + {'0', "#000000"}, {'1', "#0000AA"}, {'2', "#00AA00"}, {'3', "#00AAAA"}, {'4', "#AA0000"}, + {'5', "#AA00AA"}, {'6', "#FFAA00"}, {'7', "#AAAAAA"}, {'8', "#555555"}, {'9', "#5555FF"}, + {'a', "#55FF55"}, {'b', "#55FFFF"}, {'c', "#FF5555"}, {'d', "#FF55FF"}, {'e', "#FFFF55"}, + {'f', "#FFFFFF"} +}; + +void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack) +{ + setName(resource_pack.name()); + + // We have to manually set the colors for use. + // + // A color is set using §x, with x = a hex number from 0 to f. + // + // We traverse the description and, when one of those is found, we create + // a span element with that color set. + // + // TODO: Make the same logic for font formatting too. + // TODO: Wrap links inside <a> tags + + auto description = resource_pack.description(); + + QString description_parsed("<html>"); + bool in_div = false; + + auto desc_it = description.constBegin(); + while (desc_it != description.constEnd()) { + if (*desc_it == u'§') { + if (in_div) + description_parsed += "</span>"; + + auto const& num = *(++desc_it); + description_parsed += QString("<span style=\"color: %1;\">").arg(s_value_to_color.constFind(num).value()); + + in_div = true; + + desc_it++; + } + + description_parsed += *desc_it; + desc_it++; + } + + if (in_div) + description_parsed += "</span>"; + description_parsed += "</html>"; + + description_parsed.replace("\n", "<br>"); + + setDescription(description_parsed); + setImage(resource_pack.image({64, 64})); +} + +void InfoFrame::clear() +{ + setName(); + setDescription(); + setImage(); +} + +void InfoFrame::updateHiddenState() +{ + if(ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden()) + { + setHidden(true); + } + else + { + setHidden(false); + } +} + +void InfoFrame::setName(QString text) +{ + if(text.isEmpty()) + { + ui->nameLabel->setHidden(true); + } + else + { + ui->nameLabel->setText(text); + ui->nameLabel->setHidden(false); + } + updateHiddenState(); +} + +void InfoFrame::setDescription(QString text) +{ + if(text.isEmpty()) + { + ui->descriptionLabel->setHidden(true); + updateHiddenState(); + return; + } + else + { + ui->descriptionLabel->setHidden(false); + updateHiddenState(); + } + ui->descriptionLabel->setToolTip(""); + QString intermediatetext = text.trimmed(); + bool prev(false); + QChar rem('\n'); + QString finaltext; + finaltext.reserve(intermediatetext.size()); + foreach(const QChar& c, intermediatetext) + { + if(c == rem && prev){ + continue; + } + prev = c == rem; + finaltext += c; + } + QString labeltext; + labeltext.reserve(300); + if(finaltext.length() > 290) + { + ui->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->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); + } + else + { + ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); + labeltext.append(finaltext); + } + ui->descriptionLabel->setText(labeltext); +} + +void InfoFrame::setImage(QPixmap img) +{ + if (img.isNull()) { + ui->iconLabel->setHidden(true); + } else { + ui->iconLabel->setHidden(false); + ui->iconLabel->setPixmap(img); + } +} + +void InfoFrame::descriptionEllipsisHandler(QString link) +{ + if(!m_current_box) + { + m_current_box = CustomMessageBox::selectable(this, "", m_description); + connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed); + m_current_box->show(); + } + else + { + m_current_box->setText(m_description); + } +} + +void InfoFrame::boxClosed(int result) +{ + m_current_box = nullptr; +} diff --git a/launcher/ui/widgets/MCModInfoFrame.h b/launcher/ui/widgets/InfoFrame.h index 0b7ef537..70d15b1e 100644 --- a/launcher/ui/widgets/MCModInfoFrame.h +++ b/launcher/ui/widgets/InfoFrame.h @@ -16,37 +16,41 @@ #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 setImage(QPixmap img = {}); - void updateWithMod(Mod &m); void clear(); -public slots: - void modDescEllipsisHandler(const QString& link ); + void updateWithMod(Mod const& m); + void updateWithResource(Resource const& resource); + void updateWithResourcePack(ResourcePack& rp); + + 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..9e407ce9 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> @@ -22,10 +22,7 @@ <height>120</height> </size> </property> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <property name="spacing"> - <number>6</number> - </property> + <layout class="QGridLayout" name="gridLayout"> <property name="leftMargin"> <number>0</number> </property> @@ -38,8 +35,8 @@ <property name="bottomMargin"> <number>0</number> </property> - <item> - <widget class="QLabel" name="label_ModText"> + <item row="0" column="1"> + <widget class="QLabel" name="nameLabel"> <property name="text"> <string notr="true"/> </property> @@ -60,8 +57,8 @@ </property> </widget> </item> - <item> - <widget class="QLabel" name="label_ModDescription"> + <item row="1" column="1"> + <widget class="QLabel" name="descriptionLabel"> <property name="toolTip"> <string notr="true"/> </property> @@ -85,6 +82,31 @@ </property> </widget> </item> + <item row="0" column="0" rowspan="2"> + <widget class="QLabel" name="iconLabel"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>64</width> + <height>64</height> + </size> + </property> + <property name="text"> + <string notr="true"/> + </property> + <property name="scaledContents"> + <bool>false</bool> + </property> + <property name="margin"> + <number>0</number> + </property> + </widget> + </item> </layout> </widget> <resources/> diff --git a/launcher/ui/widgets/MCModInfoFrame.cpp b/launcher/ui/widgets/MCModInfoFrame.cpp deleted file mode 100644 index 7d78006b..00000000 --- a/launcher/ui/widgets/MCModInfoFrame.cpp +++ /dev/null @@ -1,168 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include <QMessageBox> -#include <QtGui> - -#include "MCModInfoFrame.h" -#include "ui_MCModInfoFrame.h" - -#include "ui/dialogs/CustomMessageBox.h" - -void MCModInfoFrame::updateWithMod(Mod &m) -{ - if (m.type() == m.MOD_FOLDER) - { - clear(); - return; - } - - QString text = ""; - QString name = ""; - if (m.name().isEmpty()) - name = m.internal_id(); - else - name = m.name(); - - if (m.homeurl().isEmpty()) - text = name; - else - text = "<a href=\"" + m.homeurl() + "\">" + name + "</a>"; - if (!m.authors().isEmpty()) - text += " by " + m.authors().join(", "); - - setModText(text); - - if (m.description().isEmpty()) - { - setModDescription(QString()); - } - else - { - setModDescription(m.description()); - } -} - -void MCModInfoFrame::clear() -{ - setModText(QString()); - setModDescription(QString()); -} - -MCModInfoFrame::MCModInfoFrame(QWidget *parent) : - QFrame(parent), - ui(new Ui::MCModInfoFrame) -{ - ui->setupUi(this); - ui->label_ModDescription->setHidden(true); - ui->label_ModText->setHidden(true); - updateHiddenState(); -} - -MCModInfoFrame::~MCModInfoFrame() -{ - delete ui; -} - -void MCModInfoFrame::updateHiddenState() -{ - if(ui->label_ModDescription->isHidden() && ui->label_ModText->isHidden()) - { - setHidden(true); - } - else - { - setHidden(false); - } -} - -void MCModInfoFrame::setModText(QString text) -{ - if(text.isEmpty()) - { - ui->label_ModText->setHidden(true); - } - else - { - ui->label_ModText->setText(text); - ui->label_ModText->setHidden(false); - } - updateHiddenState(); -} - -void MCModInfoFrame::setModDescription(QString text) -{ - if(text.isEmpty()) - { - ui->label_ModDescription->setHidden(true); - updateHiddenState(); - return; - } - else - { - ui->label_ModDescription->setHidden(false); - updateHiddenState(); - } - ui->label_ModDescription->setToolTip(""); - QString intermediatetext = text.trimmed(); - bool prev(false); - QChar rem('\n'); - QString finaltext; - finaltext.reserve(intermediatetext.size()); - foreach(const QChar& c, intermediatetext) - { - if(c == rem && prev){ - continue; - } - prev = c == rem; - finaltext += c; - } - QString labeltext; - labeltext.reserve(300); - if(finaltext.length() > 290) - { - ui->label_ModDescription->setOpenExternalLinks(false); - ui->label_ModDescription->setTextFormat(Qt::TextFormat::RichText); - desc = 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); - } - else - { - ui->label_ModDescription->setTextFormat(Qt::TextFormat::PlainText); - labeltext.append(finaltext); - } - ui->label_ModDescription->setText(labeltext); -} - -void MCModInfoFrame::modDescEllipsisHandler(const QString &link) -{ - if(!currentBox) - { - currentBox = CustomMessageBox::selectable(this, QString(), desc); - connect(currentBox, &QMessageBox::finished, this, &MCModInfoFrame::boxClosed); - currentBox->show(); - } - else - { - currentBox->setText(desc); - } -} - -void MCModInfoFrame::boxClosed(int result) -{ - currentBox = nullptr; -} diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index 4ab34375..ea052c41 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -1,6 +1,39 @@ #include "ModFilterWidget.h" #include "ui_ModFilterWidget.h" +#include "Application.h" + +unique_qobject_ptr<ModFilterWidget> ModFilterWidget::create(Version default_version, QWidget* parent) +{ + auto filter_widget = new ModFilterWidget(default_version, parent); + + if (!filter_widget->versionList()->isLoaded()) { + QEventLoop load_version_list_loop; + + QTimer time_limit_for_list_load; + time_limit_for_list_load.setTimerType(Qt::TimerType::CoarseTimer); + time_limit_for_list_load.setSingleShot(true); + time_limit_for_list_load.callOnTimeout(&load_version_list_loop, &QEventLoop::quit); + time_limit_for_list_load.start(4000); + + auto task = filter_widget->versionList()->getLoadTask(); + + connect(task.get(), &Task::failed, [filter_widget]{ + filter_widget->disableVersionButton(VersionButtonID::Major, tr("failed to get version index")); + }); + connect(task.get(), &Task::finished, &load_version_list_loop, &QEventLoop::quit); + + if (!task->isRunning()) + task->start(); + + load_version_list_loop.exec(); + if (time_limit_for_list_load.isActive()) + time_limit_for_list_load.stop(); + } + + return unique_qobject_ptr<ModFilterWidget>(filter_widget); +} + ModFilterWidget::ModFilterWidget(Version def, QWidget* parent) : QTabWidget(parent), m_filter(new Filter()), ui(new Ui::ModFilterWidget) { @@ -16,6 +49,7 @@ ModFilterWidget::ModFilterWidget(Version def, QWidget* parent) m_filter->versions.push_front(def); + m_version_list = APPLICATION->metadataIndex()->get("net.minecraft"); setHidden(true); } @@ -51,24 +85,30 @@ auto ModFilterWidget::getFilter() -> std::shared_ptr<Filter> return m_filter; } -void ModFilterWidget::disableVersionButton(VersionButtonID id) +void ModFilterWidget::disableVersionButton(VersionButtonID id, QString reason) { + QAbstractButton* btn = nullptr; + switch(id){ case(VersionButtonID::Strict): - ui->strictVersionButton->setEnabled(false); + btn = ui->strictVersionButton; break; case(VersionButtonID::Major): - ui->majorVersionButton->setEnabled(false); + btn = ui->majorVersionButton; break; case(VersionButtonID::All): - ui->allVersionsButton->setEnabled(false); + btn = ui->allVersionsButton; break; case(VersionButtonID::Between): - // ui->betweenVersionsButton->setEnabled(false); - break; default: break; } + + if (btn) { + btn->setEnabled(false); + if (!reason.isEmpty()) + btn->setText(btn->text() + QString(" (%1)").arg(reason)); + } } void ModFilterWidget::onVersionFilterChanged(int id) @@ -76,7 +116,7 @@ void ModFilterWidget::onVersionFilterChanged(int id) //ui->lowerVersionComboBox->setEnabled(id == VersionButtonID::Between); //ui->upperVersionComboBox->setEnabled(id == VersionButtonID::Between); - int index = 0; + int index = 1; auto cast_id = (VersionButtonID) id; if (cast_id != m_version_id) { @@ -93,10 +133,15 @@ void ModFilterWidget::onVersionFilterChanged(int id) break; case(VersionButtonID::Major): { auto versionSplit = mcVersionStr().split("."); - for(auto i = Version(QString("%1.%2").arg(versionSplit[0], versionSplit[1])); i <= mcVersion(); index++){ - m_filter->versions.push_front(i); - i = Version(QString("%1.%2.%3").arg(versionSplit[0], versionSplit[1], QString("%1").arg(index))); + + auto major_version = QString("%1.%2").arg(versionSplit[0], versionSplit[1]); + QString version_str = major_version; + + while (m_version_list->hasVersion(version_str)) { + m_filter->versions.emplace_back(version_str); + version_str = QString("%1.%2").arg(major_version, QString::number(index++)); } + break; } case(VersionButtonID::All): diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index 334fc672..958a1e2b 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -4,6 +4,10 @@ #include <QButtonGroup> #include "Version.h" + +#include "meta/Index.h" +#include "meta/VersionList.h" + #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" @@ -34,18 +38,22 @@ public: std::shared_ptr<Filter> m_filter; public: - explicit ModFilterWidget(Version def, QWidget* parent = nullptr); + static unique_qobject_ptr<ModFilterWidget> create(Version default_version, QWidget* parent = nullptr); ~ModFilterWidget(); void setInstance(MinecraftInstance* instance); /// By default all buttons are enabled - void disableVersionButton(VersionButtonID); + void disableVersionButton(VersionButtonID, QString reason = {}); auto getFilter() -> std::shared_ptr<Filter>; auto changed() const -> bool { return m_last_version_id != m_version_id; } + Meta::VersionListPtr versionList() { return m_version_list; } + private: + ModFilterWidget(Version def, QWidget* parent = nullptr); + inline auto mcVersionStr() const -> QString { return m_instance ? m_instance->getPackProfile()->getComponentVersion("net.minecraft") : ""; } inline auto mcVersion() const -> Version { return { mcVersionStr() }; } @@ -61,8 +69,12 @@ private: MinecraftInstance* m_instance = nullptr; + +/* Version stuff */ QButtonGroup m_mcVersion_buttons; + Meta::VersionListPtr m_version_list; + /* Used to tell if the filter was changed since the last getFilter() call */ VersionButtonID m_last_version_id = VersionButtonID::Strict; VersionButtonID m_version_id = VersionButtonID::Strict; diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 419ccb66..8d606820 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -244,7 +244,14 @@ void PageContainer::help() void PageContainer::currentChanged(const QModelIndex ¤t) { - showPage(current.isValid() ? m_proxyModel->mapToSource(current).row() : -1); + int selected_index = current.isValid() ? m_proxyModel->mapToSource(current).row() : -1; + + auto* selected = m_model->pages().at(selected_index); + auto* previous = m_currentPage; + + emit selectedPageChanged(previous, selected); + + showPage(selected_index); } bool PageContainer::prepareToClose() diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h index 86f549eb..80d87a9b 100644 --- a/launcher/ui/widgets/PageContainer.h +++ b/launcher/ui/widgets/PageContainer.h @@ -95,6 +95,10 @@ private: public slots: void help(); +signals: + /** Emitted when the currently selected page is changed */ + void selectedPageChanged(BasePage* previous, BasePage* selected); + private slots: void currentChanged(const QModelIndex ¤t); void showPage(int row); diff --git a/launcher/ui/widgets/ProgressWidget.cpp b/launcher/ui/widgets/ProgressWidget.cpp index 911e555d..b60d9a7a 100644 --- a/launcher/ui/widgets/ProgressWidget.cpp +++ b/launcher/ui/widgets/ProgressWidget.cpp @@ -1,66 +1,104 @@ // Licensed under the Apache-2.0 license. See README.md for details. #include "ProgressWidget.h" -#include <QProgressBar> +#include <QEventLoop> #include <QLabel> +#include <QProgressBar> #include <QVBoxLayout> -#include <QEventLoop> #include "tasks/Task.h" -ProgressWidget::ProgressWidget(QWidget *parent) - : QWidget(parent) +ProgressWidget::ProgressWidget(QWidget* parent, bool show_label) : QWidget(parent) { - m_label = new QLabel(this); - m_label->setWordWrap(true); + auto* layout = new QVBoxLayout(this); + + if (show_label) { + m_label = new QLabel(this); + m_label->setWordWrap(true); + layout->addWidget(m_label); + } + m_bar = new QProgressBar(this); m_bar->setMinimum(0); m_bar->setMaximum(100); - QVBoxLayout *layout = new QVBoxLayout(this); - layout->addWidget(m_label); layout->addWidget(m_bar); - layout->addStretch(); + setLayout(layout); } -void ProgressWidget::start(std::shared_ptr<Task> task) +void ProgressWidget::reset() +{ + m_bar->reset(); +} + +void ProgressWidget::progressFormat(QString format) +{ + if (format.isEmpty()) + m_bar->setTextVisible(false); + else + m_bar->setFormat(format); +} + +void ProgressWidget::watch(Task* task) { + if (!task) + return; + if (m_task) - { - disconnect(m_task.get(), 0, this, 0); - } + disconnect(m_task, nullptr, this, nullptr); + m_task = task; - connect(m_task.get(), &Task::finished, this, &ProgressWidget::handleTaskFinish); - connect(m_task.get(), &Task::status, this, &ProgressWidget::handleTaskStatus); - connect(m_task.get(), &Task::progress, this, &ProgressWidget::handleTaskProgress); - connect(m_task.get(), &Task::destroyed, this, &ProgressWidget::taskDestroyed); + + connect(m_task, &Task::finished, this, &ProgressWidget::handleTaskFinish); + connect(m_task, &Task::status, this, &ProgressWidget::handleTaskStatus); + connect(m_task, &Task::progress, this, &ProgressWidget::handleTaskProgress); + connect(m_task, &Task::destroyed, this, &ProgressWidget::taskDestroyed); + + show(); +} + +void ProgressWidget::start(Task* task) +{ + watch(task); if (!m_task->isRunning()) - { - QMetaObject::invokeMethod(m_task.get(), "start", Qt::QueuedConnection); - } + QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection); } + bool ProgressWidget::exec(std::shared_ptr<Task> task) { QEventLoop loop; + connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); - start(task); + + start(task.get()); + if (task->isRunning()) - { loop.exec(); - } + return task->wasSuccessful(); } +void ProgressWidget::show() +{ + setHidden(false); +} +void ProgressWidget::hide() +{ + setHidden(true); +} + void ProgressWidget::handleTaskFinish() { - if (!m_task->wasSuccessful()) - { + if (!m_task->wasSuccessful() && m_label) m_label->setText(m_task->failReason()); - } + + if (m_hide_if_inactive) + hide(); } -void ProgressWidget::handleTaskStatus(const QString &status) +void ProgressWidget::handleTaskStatus(const QString& status) { - m_label->setText(status); + if (m_label) + m_label->setText(status); } void ProgressWidget::handleTaskProgress(qint64 current, qint64 total) { diff --git a/launcher/ui/widgets/ProgressWidget.h b/launcher/ui/widgets/ProgressWidget.h index fa67748a..4d9097b8 100644 --- a/launcher/ui/widgets/ProgressWidget.h +++ b/launcher/ui/widgets/ProgressWidget.h @@ -9,24 +9,48 @@ class Task; class QProgressBar; class QLabel; -class ProgressWidget : public QWidget -{ +class ProgressWidget : public QWidget { Q_OBJECT -public: - explicit ProgressWidget(QWidget *parent = nullptr); + public: + explicit ProgressWidget(QWidget* parent = nullptr, bool show_label = true); -public slots: - void start(std::shared_ptr<Task> task); + /** Whether to hide the widget automatically if it's watching no running task. */ + void hideIfInactive(bool hide) { m_hide_if_inactive = hide; } + + /** Reset the displayed progress to 0 */ + void reset(); + + /** The text that shows up in the middle of the progress bar. + * By default it's '%p%', with '%p' being the total progress in percentage. + */ + void progressFormat(QString); + + public slots: + /** Watch the progress of a task. */ + void watch(Task* task); + + /** Watch the progress of a task, and start it if needed */ + void start(Task* task); + + /** Blocking way of waiting for a task to finish. */ bool exec(std::shared_ptr<Task> task); -private slots: + /** Un-hide the widget if needed. */ + void show(); + + /** Make the widget invisible. */ + void hide(); + + private slots: void handleTaskFinish(); - void handleTaskStatus(const QString &status); + void handleTaskStatus(const QString& status); void handleTaskProgress(qint64 current, qint64 total); void taskDestroyed(); -private: - QLabel *m_label; - QProgressBar *m_bar; - std::shared_ptr<Task> m_task; + private: + QLabel* m_label = nullptr; + QProgressBar* m_bar = nullptr; + Task* m_task = nullptr; + + bool m_hide_if_inactive = false; }; diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp new file mode 100644 index 00000000..56ae35fb --- /dev/null +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -0,0 +1,78 @@ +#include "ProjectItem.h" + +#include "Common.h" + +#include <QIcon> +#include <QPainter> + +ProjectItemDelegate::ProjectItemDelegate(QWidget* parent) : QStyledItemDelegate(parent) {} + +void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const +{ + painter->save(); + + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + + auto& rect = opt.rect; + auto icon_width = rect.height(), icon_height = rect.height(); + auto remaining_width = rect.width() - icon_width; + + if (opt.state & QStyle::State_Selected) { + painter->fillRect(rect, opt.palette.highlight()); + painter->setPen(opt.palette.highlightedText().color()); + } else if (opt.state & QStyle::State_MouseOver) { + painter->fillRect(rect, opt.palette.window()); + } + + { // Icon painting + // Square-sized, occupying the left portion + opt.icon.paint(painter, rect.x(), rect.y(), icon_width, icon_height); + } + + { // Title painting + auto title = index.data(UserDataTypes::TITLE).toString(); + + painter->save(); + + auto font = opt.font; + if (index.data(UserDataTypes::SELECTED).toBool()) { + // Set nice font + font.setBold(true); + font.setUnderline(true); + } + + font.setPointSize(font.pointSize() + 2); + painter->setFont(font); + + // On the top, aligned to the left after the icon + painter->drawText(rect.x() + icon_width, rect.y() + QFontMetrics(font).height(), title); + + painter->restore(); + } + + { // Description painting + auto description = index.data(UserDataTypes::DESCRIPTION).toString(); + + QTextLayout text_layout(description, opt.font); + + qreal height = 0; + auto cut_text = viewItemTextLayout(text_layout, remaining_width, height); + + // Get first line unconditionally + description = cut_text.first().second; + // Get second line, elided if needed + if (cut_text.size() > 1) { + if (cut_text.size() > 2) + description += opt.fontMetrics.elidedText(cut_text.at(1).second, opt.textElideMode, cut_text.at(1).first); + else + description += cut_text.at(1).second; + } + + // On the bottom, aligned to the left after the icon, and featuring at most two lines of text (with some margin space to spare) + painter->drawText(rect.x() + icon_width, rect.y() + rect.height() - 2.2 * opt.fontMetrics.height(), remaining_width, + 2 * opt.fontMetrics.height(), Qt::TextWordWrap, description); + } + + painter->restore(); +} diff --git a/launcher/ui/widgets/ProjectItem.h b/launcher/ui/widgets/ProjectItem.h new file mode 100644 index 00000000..f668edf6 --- /dev/null +++ b/launcher/ui/widgets/ProjectItem.h @@ -0,0 +1,25 @@ +#pragma once + +#include <QStyledItemDelegate> + +/* Custom data types for our custom list models :) */ +enum UserDataTypes { + TITLE = 257, // QString + DESCRIPTION = 258, // QString + SELECTED = 259 // bool +}; + +/** This is an item delegate composed of: + * - An Icon on the left + * - A title + * - A description + * */ +class ProjectItemDelegate final : public QStyledItemDelegate { + Q_OBJECT + + public: + ProjectItemDelegate(QWidget* parent); + + void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override; + +}; 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 946e34d8..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). @@ -155,28 +159,21 @@ Canonical implementation of the murmur2 hash, taken from [SMHasher](https://gith Public domain (the author disclaimed the copyright). -## optional-bare - -A simple single-file header-only version of a C++17-like optional for default-constructible, copyable types, for C++98 and later. - -Imported from: https://github.com/martinmoene/optional-bare/commit/0bb1d183bcee1e854c4ea196b533252c51f98b81 - -Boost Software License - Version 1.0 - ## 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 @@ -190,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/optional-bare/CMakeLists.txt b/libraries/optional-bare/CMakeLists.txt deleted file mode 100644 index 952df6e2..00000000 --- a/libraries/optional-bare/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -cmake_minimum_required(VERSION 3.9.4) -project(optional-bare) - -add_library(optional-bare INTERFACE) -target_include_directories(optional-bare INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") diff --git a/libraries/optional-bare/LICENSE.txt b/libraries/optional-bare/LICENSE.txt deleted file mode 100644 index 36b7cd93..00000000 --- a/libraries/optional-bare/LICENSE.txt +++ /dev/null @@ -1,23 +0,0 @@ -Boost Software License - Version 1.0 - August 17th, 2003 - -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: - -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -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, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/libraries/optional-bare/README.md b/libraries/optional-bare/README.md deleted file mode 100644 index e29ff7c1..00000000 --- a/libraries/optional-bare/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# optional bare - -A simple single-file header-only version of a C++17-like optional for default-constructible, copyable types, for C++98 and later. - -Imported from: https://github.com/martinmoene/optional-bare/commit/0bb1d183bcee1e854c4ea196b533252c51f98b81 diff --git a/libraries/optional-bare/include/nonstd/optional b/libraries/optional-bare/include/nonstd/optional deleted file mode 100644 index ecbfa030..00000000 --- a/libraries/optional-bare/include/nonstd/optional +++ /dev/null @@ -1,508 +0,0 @@ -// -// Copyright 2017-2019 by Martin Moene -// -// https://github.com/martinmoene/optional-bare -// -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) - -#ifndef NONSTD_OPTIONAL_BARE_HPP -#define NONSTD_OPTIONAL_BARE_HPP - -#define optional_bare_MAJOR 1 -#define optional_bare_MINOR 1 -#define optional_bare_PATCH 0 - -#define optional_bare_VERSION optional_STRINGIFY(optional_bare_MAJOR) "." optional_STRINGIFY(optional_bare_MINOR) "." optional_STRINGIFY(optional_bare_PATCH) - -#define optional_STRINGIFY( x ) optional_STRINGIFY_( x ) -#define optional_STRINGIFY_( x ) #x - -// optional-bare configuration: - -#define optional_OPTIONAL_DEFAULT 0 -#define optional_OPTIONAL_NONSTD 1 -#define optional_OPTIONAL_STD 2 - -#if !defined( optional_CONFIG_SELECT_OPTIONAL ) -# define optional_CONFIG_SELECT_OPTIONAL ( optional_HAVE_STD_OPTIONAL ? optional_OPTIONAL_STD : optional_OPTIONAL_NONSTD ) -#endif - -// Control presence of exception handling (try and auto discover): - -#ifndef optional_CONFIG_NO_EXCEPTIONS -# if _MSC_VER -# include <cstddef> // for _HAS_EXCEPTIONS -# endif -# if _MSC_VER -# include <cstddef> // for _HAS_EXCEPTIONS -# endif -# if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || (_HAS_EXCEPTIONS) -# define optional_CONFIG_NO_EXCEPTIONS 0 -# else -# define optional_CONFIG_NO_EXCEPTIONS 1 -# endif -#endif - -// C++ language version detection (C++20 is speculative): -// Note: VC14.0/1900 (VS2015) lacks too much from C++14. - -#ifndef optional_CPLUSPLUS -# if defined(_MSVC_LANG ) && !defined(__clang__) -# define optional_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG ) -# else -# define optional_CPLUSPLUS __cplusplus -# endif -#endif - -#define optional_CPP98_OR_GREATER ( optional_CPLUSPLUS >= 199711L ) -#define optional_CPP11_OR_GREATER ( optional_CPLUSPLUS >= 201103L ) -#define optional_CPP14_OR_GREATER ( optional_CPLUSPLUS >= 201402L ) -#define optional_CPP17_OR_GREATER ( optional_CPLUSPLUS >= 201703L ) -#define optional_CPP20_OR_GREATER ( optional_CPLUSPLUS >= 202000L ) - -// C++ language version (represent 98 as 3): - -#define optional_CPLUSPLUS_V ( optional_CPLUSPLUS / 100 - (optional_CPLUSPLUS > 200000 ? 2000 : 1994) ) - -// Use C++17 std::optional if available and requested: - -#if optional_CPP17_OR_GREATER && defined(__has_include ) -# if __has_include( <optional> ) -# define optional_HAVE_STD_OPTIONAL 1 -# else -# define optional_HAVE_STD_OPTIONAL 0 -# endif -#else -# define optional_HAVE_STD_OPTIONAL 0 -#endif - -#define optional_USES_STD_OPTIONAL ( (optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_STD) || ((optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_DEFAULT) && optional_HAVE_STD_OPTIONAL) ) - -// -// Using std::optional: -// - -#if optional_USES_STD_OPTIONAL - -#include <optional> -#include <utility> - -namespace nonstd { - - using std::in_place; - using std::in_place_type; - using std::in_place_index; - using std::in_place_t; - using std::in_place_type_t; - using std::in_place_index_t; - - using std::optional; - using std::bad_optional_access; - using std::hash; - - using std::nullopt; - using std::nullopt_t; - - using std::operator==; - using std::operator!=; - using std::operator<; - using std::operator<=; - using std::operator>; - using std::operator>=; - using std::make_optional; - using std::swap; -} - -#else // optional_USES_STD_OPTIONAL - -#include <cassert> - -#if ! optional_CONFIG_NO_EXCEPTIONS -# include <stdexcept> -#endif - -namespace nonstd { namespace optional_bare { - -// type for nullopt - -struct nullopt_t -{ - struct init{}; - nullopt_t( init ) {} -}; - -// extra parenthesis to prevent the most vexing parse: - -const nullopt_t nullopt(( nullopt_t::init() )); - -// optional access error. - -#if ! optional_CONFIG_NO_EXCEPTIONS - -class bad_optional_access : public std::logic_error -{ -public: - explicit bad_optional_access() - : logic_error( "bad optional access" ) {} -}; - -#endif // optional_CONFIG_NO_EXCEPTIONS - -// Simplistic optional: requires T to be default constructible, copyable. - -template< typename T > -class optional -{ -private: - typedef void (optional::*safe_bool)() const; - -public: - typedef T value_type; - - optional() - : has_value_( false ) - {} - - optional( nullopt_t ) - : has_value_( false ) - {} - - optional( T const & arg ) - : has_value_( true ) - , value_ ( arg ) - {} - - template< class U > - optional( optional<U> const & other ) - : has_value_( other.has_value() ) - , value_ ( other.value() ) - {} - - optional & operator=( nullopt_t ) - { - reset(); - return *this; - } - - template< class U > - optional & operator=( optional<U> const & other ) - { - has_value_ = other.has_value(); - value_ = other.value(); - return *this; - } - - void swap( optional & rhs ) - { - using std::swap; - if ( has_value() == true && rhs.has_value() == true ) { swap( **this, *rhs ); } - else if ( has_value() == false && rhs.has_value() == true ) { initialize( *rhs ); rhs.reset(); } - else if ( has_value() == true && rhs.has_value() == false ) { rhs.initialize( **this ); reset(); } - } - - // observers - - value_type const * operator->() const - { - return assert( has_value() ), - &value_; - } - - value_type * operator->() - { - return assert( has_value() ), - &value_; - } - - value_type const & operator*() const - { - return assert( has_value() ), - value_; - } - - value_type & operator*() - { - return assert( has_value() ), - value_; - } - -#if optional_CPP11_OR_GREATER - explicit operator bool() const - { - return has_value(); - } -#else - operator safe_bool() const - { - return has_value() ? &optional::this_type_does_not_support_comparisons : 0; - } -#endif - - bool has_value() const - { - return has_value_; - } - - value_type const & value() const - { -#if optional_CONFIG_NO_EXCEPTIONS - assert( has_value() ); -#else - if ( ! has_value() ) - throw bad_optional_access(); -#endif - return value_; - } - - value_type & value() - { -#if optional_CONFIG_NO_EXCEPTIONS - assert( has_value() ); -#else - if ( ! has_value() ) - throw bad_optional_access(); -#endif - return value_; - } - - template< class U > - value_type value_or( U const & v ) const - { - return has_value() ? value() : static_cast<value_type>( v ); - } - - // modifiers - - void reset() - { - has_value_ = false; - } - -private: - void this_type_does_not_support_comparisons() const {} - - template< typename V > - void initialize( V const & value ) - { - assert( ! has_value() ); - value_ = value; - has_value_ = true; - } - -private: - bool has_value_; - value_type value_; -}; - -// Relational operators - -template< typename T, typename U > -inline bool operator==( optional<T> const & x, optional<U> const & y ) -{ - return bool(x) != bool(y) ? false : bool(x) == false ? true : *x == *y; -} - -template< typename T, typename U > -inline bool operator!=( optional<T> const & x, optional<U> const & y ) -{ - return !(x == y); -} - -template< typename T, typename U > -inline bool operator<( optional<T> const & x, optional<U> const & y ) -{ - return (!y) ? false : (!x) ? true : *x < *y; -} - -template< typename T, typename U > -inline bool operator>( optional<T> const & x, optional<U> const & y ) -{ - return (y < x); -} - -template< typename T, typename U > -inline bool operator<=( optional<T> const & x, optional<U> const & y ) -{ - return !(y < x); -} - -template< typename T, typename U > -inline bool operator>=( optional<T> const & x, optional<U> const & y ) -{ - return !(x < y); -} - -// Comparison with nullopt - -template< typename T > -inline bool operator==( optional<T> const & x, nullopt_t ) -{ - return (!x); -} - -template< typename T > -inline bool operator==( nullopt_t, optional<T> const & x ) -{ - return (!x); -} - -template< typename T > -inline bool operator!=( optional<T> const & x, nullopt_t ) -{ - return bool(x); -} - -template< typename T > -inline bool operator!=( nullopt_t, optional<T> const & x ) -{ - return bool(x); -} - -template< typename T > -inline bool operator<( optional<T> const &, nullopt_t ) -{ - return false; -} - -template< typename T > -inline bool operator<( nullopt_t, optional<T> const & x ) -{ - return bool(x); -} - -template< typename T > -inline bool operator<=( optional<T> const & x, nullopt_t ) -{ - return (!x); -} - -template< typename T > -inline bool operator<=( nullopt_t, optional<T> const & ) -{ - return true; -} - -template< typename T > -inline bool operator>( optional<T> const & x, nullopt_t ) -{ - return bool(x); -} - -template< typename T > -inline bool operator>( nullopt_t, optional<T> const & ) -{ - return false; -} - -template< typename T > -inline bool operator>=( optional<T> const &, nullopt_t ) -{ - return true; -} - -template< typename T > -inline bool operator>=( nullopt_t, optional<T> const & x ) -{ - return (!x); -} - -// Comparison with T - -template< typename T, typename U > -inline bool operator==( optional<T> const & x, U const & v ) -{ - return bool(x) ? *x == v : false; -} - -template< typename T, typename U > -inline bool operator==( U const & v, optional<T> const & x ) -{ - return bool(x) ? v == *x : false; -} - -template< typename T, typename U > -inline bool operator!=( optional<T> const & x, U const & v ) -{ - return bool(x) ? *x != v : true; -} - -template< typename T, typename U > -inline bool operator!=( U const & v, optional<T> const & x ) -{ - return bool(x) ? v != *x : true; -} - -template< typename T, typename U > -inline bool operator<( optional<T> const & x, U const & v ) -{ - return bool(x) ? *x < v : true; -} - -template< typename T, typename U > -inline bool operator<( U const & v, optional<T> const & x ) -{ - return bool(x) ? v < *x : false; -} - -template< typename T, typename U > -inline bool operator<=( optional<T> const & x, U const & v ) -{ - return bool(x) ? *x <= v : true; -} - -template< typename T, typename U > -inline bool operator<=( U const & v, optional<T> const & x ) -{ - return bool(x) ? v <= *x : false; -} - -template< typename T, typename U > -inline bool operator>( optional<T> const & x, U const & v ) -{ - return bool(x) ? *x > v : false; -} - -template< typename T, typename U > -inline bool operator>( U const & v, optional<T> const & x ) -{ - return bool(x) ? v > *x : true; -} - -template< typename T, typename U > -inline bool operator>=( optional<T> const & x, U const & v ) -{ - return bool(x) ? *x >= v : false; -} - -template< typename T, typename U > -inline bool operator>=( U const & v, optional<T> const & x ) -{ - return bool(x) ? v >= *x : true; -} - -// Specialized algorithms - -template< typename T > -void swap( optional<T> & x, optional<T> & y ) -{ - x.swap( y ); -} - -// Convenience function to create an optional. - -template< typename T > -inline optional<T> make_optional( T const & v ) -{ - return optional<T>( v ); -} - -} // namespace optional-bare - -using namespace optional_bare; - -} // namespace nonstd - -#endif // optional_USES_STD_OPTIONAL - -#endif // NONSTD_OPTIONAL_BARE_HPP 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 @@ -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..ef5a7d93 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,31 +16,35 @@ <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="976" height="764">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="1103" height="954">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="1000" height="692">https://polymc.org/img/screenshots/ModInstallDark.png</image> + </screenshot> + <screenshot> + <caption>Mod updating</caption> + <image type="source" width="930" height="858">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="1083" height="735">https://polymc.org/img/screenshots/PropertiesDark.png</image> </screenshot> <screenshot> <caption>Cat :)</caption> @@ -48,7 +52,7 @@ </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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 00000000..1265d7a5 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,43 @@ +project(tests) + +ecm_add_test(FileSystem_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME FileSystem) + +ecm_add_test(GZip_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME GZip) + +ecm_add_test(GradleSpecifier_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME GradleSpecifier) + +ecm_add_test(PackageManifest_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME PackageManifest) + +ecm_add_test(MojangVersionFormat_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME MojangVersionFormat) + +ecm_add_test(Library_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME Library) + +ecm_add_test(ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME ResourceFolderModel) + +ecm_add_test(ResourcePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME ResourcePackParse) + +ecm_add_test(ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME ParseUtils) + +ecm_add_test(Task_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME Task) + +ecm_add_test(INIFile_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME INIFile) + +ecm_add_test(JavaVersion_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME JavaVersion) + +ecm_add_test(Packwiz_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME Packwiz) + +ecm_add_test(Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME Index) diff --git a/launcher/FileSystem_test.cpp b/tests/FileSystem_test.cpp index 99ae9269..6df13e80 100644 --- a/launcher/FileSystem_test.cpp +++ b/tests/FileSystem_test.cpp @@ -2,7 +2,7 @@ #include <QTemporaryDir> #include <QStandardPaths> -#include "FileSystem.h" +#include <FileSystem.h> class FileSystemTest : public QObject { @@ -80,7 +80,7 @@ slots: void test_copy() { - QString folder = QFINDTESTDATA("testdata/test_folder"); + QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; diff --git a/launcher/GZip_test.cpp b/tests/GZip_test.cpp index 73859fbc..1e762b2e 100644 --- a/launcher/GZip_test.cpp +++ b/tests/GZip_test.cpp @@ -1,6 +1,6 @@ #include <QTest> -#include "GZip.h" +#include <GZip.h> #include <random> void fib(int &prev, int &cur) diff --git a/launcher/minecraft/GradleSpecifier_test.cpp b/tests/GradleSpecifier_test.cpp index a062dfac..850f8388 100644 --- a/launcher/minecraft/GradleSpecifier_test.cpp +++ b/tests/GradleSpecifier_test.cpp @@ -1,6 +1,6 @@ #include <QTest> -#include "minecraft/GradleSpecifier.h" +#include <minecraft/GradleSpecifier.h> class GradleSpecifierTest : public QObject { diff --git a/launcher/settings/INIFile_test.cpp b/tests/INIFile_test.cpp index d23f9fdf..b64b031b 100644 --- a/launcher/settings/INIFile_test.cpp +++ b/tests/INIFile_test.cpp @@ -1,6 +1,6 @@ #include <QTest> -#include "settings/INIFile.h" +#include <settings/INIFile.h> class IniFileTest : public QObject { diff --git a/launcher/meta/Index_test.cpp b/tests/Index_test.cpp index 261858c4..436b753e 100644 --- a/launcher/meta/Index_test.cpp +++ b/tests/Index_test.cpp @@ -1,7 +1,7 @@ #include <QTest> -#include "meta/Index.h" -#include "meta/VersionList.h" +#include <meta/Index.h> +#include <meta/VersionList.h> class IndexTest : public QObject { diff --git a/launcher/java/JavaVersion_test.cpp b/tests/JavaVersion_test.cpp index 545947ef..76d9af2f 100644 --- a/launcher/java/JavaVersion_test.cpp +++ b/tests/JavaVersion_test.cpp @@ -1,6 +1,6 @@ #include <QTest> -#include "java/JavaVersion.h" +#include <java/JavaVersion.h> class JavaVersionTest : public QObject { diff --git a/launcher/minecraft/Library_test.cpp b/tests/Library_test.cpp index 834dd558..869c7673 100644 --- a/launcher/minecraft/Library_test.cpp +++ b/tests/Library_test.cpp @@ -1,10 +1,10 @@ #include <QTest> -#include "minecraft/MojangVersionFormat.h" -#include "minecraft/OneSixVersionFormat.h" -#include "minecraft/Library.h" -#include "net/HttpMetaCache.h" -#include "FileSystem.h" +#include <minecraft/MojangVersionFormat.h> +#include <minecraft/OneSixVersionFormat.h> +#include <minecraft/Library.h> +#include <net/HttpMetaCache.h> +#include <FileSystem.h> class LibraryTest : public QObject { @@ -30,7 +30,7 @@ slots: { cache.reset(new HttpMetaCache()); cache->addBase("libraries", QDir("libraries").absolutePath()); - dataDir = QDir(QFINDTESTDATA("testdata")).absolutePath(); + dataDir = QDir(QFINDTESTDATA("testdata/Library")).absolutePath(); } void test_legacy() { @@ -72,14 +72,14 @@ slots: QCOMPARE(test.isNative(), false); QStringList failedFiles; test.setHint("local"); - auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QFINDTESTDATA("testdata")); + auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QFINDTESTDATA("testdata/Library")); QCOMPARE(downloads.size(), 0); qDebug() << failedFiles; QCOMPARE(failedFiles.size(), 0); QStringList jar, native, native32, native64; - test.getApplicableFiles(currentSystem, jar, native, native32, native64, QFINDTESTDATA("testdata")); - QCOMPARE(jar, {QFileInfo(QFINDTESTDATA("testdata/codecwav-20101023.jar")).absoluteFilePath()}); + test.getApplicableFiles(currentSystem, jar, native, native32, native64, QFINDTESTDATA("testdata/Library")); + QCOMPARE(jar, {QFileInfo(QFINDTESTDATA("testdata/Library/codecwav-20101023.jar")).absoluteFilePath()}); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); @@ -165,20 +165,20 @@ slots: test.setRepositoryURL("file://foo/bar"); { QStringList jar, native, native32, native64; - test.getApplicableFiles(Os_Linux, jar, native, native32, native64, QFINDTESTDATA("testdata")); + test.getApplicableFiles(Os_Linux, jar, native, native32, native64, QFINDTESTDATA("testdata/Library")); QCOMPARE(jar, {}); QCOMPARE(native, {}); - QCOMPARE(native32, {QFileInfo(QFINDTESTDATA("testdata/testname-testversion-linux-32.jar")).absoluteFilePath()}); - QCOMPARE(native64, {QFileInfo(QFINDTESTDATA("testdata") + "/testname-testversion-linux-64.jar").absoluteFilePath()}); + QCOMPARE(native32, {QFileInfo(QFINDTESTDATA("testdata/Library/testname-testversion-linux-32.jar")).absoluteFilePath()}); + QCOMPARE(native64, {QFileInfo(QFINDTESTDATA("testdata/Library") + "/testname-testversion-linux-64.jar").absoluteFilePath()}); QStringList failedFiles; - auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QFINDTESTDATA("testdata")); + auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QFINDTESTDATA("testdata/Library")); QCOMPARE(dls.size(), 0); - QCOMPARE(failedFiles, {QFileInfo(QFINDTESTDATA("testdata") + "/testname-testversion-linux-64.jar").absoluteFilePath()}); + QCOMPARE(failedFiles, {QFileInfo(QFINDTESTDATA("testdata/Library") + "/testname-testversion-linux-64.jar").absoluteFilePath()}); } } void test_onenine() { - auto test = readMojangJson(QFINDTESTDATA("testdata/lib-simple.json")); + auto test = readMojangJson(QFINDTESTDATA("testdata/Library/lib-simple.json")); { QStringList jar, native, native32, native64; test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString()); @@ -197,41 +197,41 @@ slots: test->setHint("local"); { QStringList jar, native, native32, native64; - test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QFINDTESTDATA("testdata")); - QCOMPARE(jar, {QFileInfo(QFINDTESTDATA("testdata/codecwav-20101023.jar")).absoluteFilePath()}); + test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QFINDTESTDATA("testdata/Library")); + QCOMPARE(jar, {QFileInfo(QFINDTESTDATA("testdata/Library/codecwav-20101023.jar")).absoluteFilePath()}); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); } { QStringList failedFiles; - auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QFINDTESTDATA("testdata")); + auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QFINDTESTDATA("testdata/Library")); QCOMPARE(dls.size(), 0); QCOMPARE(failedFiles, {}); } } void test_onenine_local_override() { - auto test = readMojangJson(QFINDTESTDATA("testdata/lib-simple.json")); + auto test = readMojangJson(QFINDTESTDATA("testdata/Library/lib-simple.json")); test->setHint("local"); { QStringList jar, native, native32, native64; - test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QFINDTESTDATA("testdata")); - QCOMPARE(jar, {QFileInfo(QFINDTESTDATA("testdata/codecwav-20101023.jar")).absoluteFilePath()}); + test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QFINDTESTDATA("testdata/Library")); + QCOMPARE(jar, {QFileInfo(QFINDTESTDATA("testdata/Library/codecwav-20101023.jar")).absoluteFilePath()}); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); } { QStringList failedFiles; - auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QFINDTESTDATA("testdata")); + auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QFINDTESTDATA("testdata/Library")); QCOMPARE(dls.size(), 0); QCOMPARE(failedFiles, {}); } } void test_onenine_native() { - auto test = readMojangJson(QFINDTESTDATA("testdata/lib-native.json")); + auto test = readMojangJson(QFINDTESTDATA("testdata/Library/lib-native.json")); QStringList jar, native, native32, native64; test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString()); QCOMPARE(jar, QStringList()); @@ -246,7 +246,7 @@ slots: } void test_onenine_native_arch() { - auto test = readMojangJson(QFINDTESTDATA("testdata/lib-native-arch.json")); + auto test = readMojangJson(QFINDTESTDATA("testdata/Library/lib-native-arch.json")); QStringList jar, native, native32, native64; test->getApplicableFiles(Os_Windows, jar, native, native32, native64, QString()); QCOMPARE(jar, {}); diff --git a/launcher/minecraft/MojangVersionFormat_test.cpp b/tests/MojangVersionFormat_test.cpp index 71df784b..219fbfa2 100644 --- a/launcher/minecraft/MojangVersionFormat_test.cpp +++ b/tests/MojangVersionFormat_test.cpp @@ -1,7 +1,7 @@ #include <QTest> #include <QDebug> -#include "minecraft/MojangVersionFormat.h" +#include <minecraft/MojangVersionFormat.h> class MojangVersionFormatTest : public QObject { @@ -29,7 +29,7 @@ private slots: void test_Through_Simple() { - QJsonDocument doc = readJson(QFINDTESTDATA("testdata/1.9-simple.json")); + QJsonDocument doc = readJson(QFINDTESTDATA("testdata/MojangVersionFormat/1.9-simple.json")); auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9-simple.json"); auto doc2 = MojangVersionFormat::versionFileToJson(vfile); writeJson("1.9-simple-passthorugh.json", doc2); @@ -39,7 +39,7 @@ slots: void test_Through() { - QJsonDocument doc = readJson(QFINDTESTDATA("testdata/1.9.json")); + QJsonDocument doc = readJson(QFINDTESTDATA("testdata/MojangVersionFormat/1.9.json")); auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9.json"); auto doc2 = MojangVersionFormat::versionFileToJson(vfile); writeJson("1.9-passthorugh.json", doc2); diff --git a/launcher/mojang/PackageManifest_test.cpp b/tests/PackageManifest_test.cpp index e8da4266..e38abf80 100644 --- a/launcher/mojang/PackageManifest_test.cpp +++ b/tests/PackageManifest_test.cpp @@ -1,7 +1,7 @@ #include <QTest> #include <QDebug> -#include "mojang/PackageManifest.h" +#include <mojang/PackageManifest.h> using namespace mojang_files; @@ -82,14 +82,14 @@ void PackageManifestTest::test_parse() } void PackageManifestTest::test_parse_file() { - auto path = QFINDTESTDATA("testdata/1.8.0_202-x64.json"); + auto path = QFINDTESTDATA("testdata/PackageManifest/1.8.0_202-x64.json"); auto manifest = Package::fromManifestFile(path); QVERIFY(manifest.valid == true); } void PackageManifestTest::test_inspect() { - auto path = QFINDTESTDATA("testdata/inspect_win/"); + auto path = QFINDTESTDATA("testdata/PackageManifest/inspect_win/"); auto manifest = Package::fromInspectedFolder(path); QVERIFY(manifest.valid == true); QVERIFY(manifest.files.size() == 2); @@ -112,7 +112,7 @@ void PackageManifestTest::test_inspect() { #ifndef Q_OS_WIN32 void PackageManifestTest::test_inspect_symlinks() { - auto path = QFINDTESTDATA("testdata/inspect/"); + auto path = QFINDTESTDATA("testdata/PackageManifest/inspect/"); auto manifest = Package::fromInspectedFolder(path); QVERIFY(manifest.valid == true); QVERIFY(manifest.files.size() == 1); diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/tests/Packwiz_test.cpp index aa0c35df..098e8f89 100644 --- a/launcher/modplatform/packwiz/Packwiz_test.cpp +++ b/tests/Packwiz_test.cpp @@ -20,7 +20,7 @@ #include <QTemporaryDir> #include <QTest> -#include "Packwiz.h" +#include <modplatform/packwiz/Packwiz.h> class PackwizTest : public QObject { Q_OBJECT @@ -29,7 +29,7 @@ class PackwizTest : public QObject { // Files taken from https://github.com/packwiz/packwiz-example-pack void loadFromFile_Modrinth() { - QString source = QFINDTESTDATA("testdata"); + QString source = QFINDTESTDATA("testdata/Packwiz"); QDir index_dir(source); QString slug_mod("borderless-mining"); @@ -55,7 +55,7 @@ class PackwizTest : public QObject { void loadFromFile_Curseforge() { - QString source = QFINDTESTDATA("testdata"); + QString source = QFINDTESTDATA("testdata/Packwiz"); QDir index_dir(source); QString name_mod("screenshot-to-clipboard-fabric.pw.toml"); diff --git a/launcher/minecraft/ParseUtils_test.cpp b/tests/ParseUtils_test.cpp index 7721a46d..02208fdf 100644 --- a/launcher/minecraft/ParseUtils_test.cpp +++ b/tests/ParseUtils_test.cpp @@ -1,6 +1,6 @@ #include <QTest> -#include "minecraft/ParseUtils.h" +#include <minecraft/ParseUtils.h> class ParseUtilsTest : public QObject { diff --git a/tests/ResourceFolderModel_test.cpp b/tests/ResourceFolderModel_test.cpp new file mode 100644 index 00000000..3f0f3ba1 --- /dev/null +++ b/tests/ResourceFolderModel_test.cpp @@ -0,0 +1,261 @@ +// 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, &loop, 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/ResourceFolderModel/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/ResourceFolderModel"); + + ModFolderModel model(source); + + QCOMPARE(model.size(), 0); + + EXEC_UPDATE_TASK(model.startWatching(), ) + + for (auto mod : model.allMods()) + qDebug() << mod->name(); + + // FIXME: It considers every file in the directory as a mod, but we should probably filter that out somehow. + QCOMPARE(model.size(), 4); + + model.stopWatching(); + } + + void test_removeResource() + { + QString folder_resource = QFINDTESTDATA("testdata/ResourceFolderModel/test_folder"); + QString file_mod = QFINDTESTDATA("testdata/ResourceFolderModel/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(); + } + + void test_enable_disable() + { + QString folder_resource = QFINDTESTDATA("testdata/ResourceFolderModel/test_folder"); + QString file_mod = QFINDTESTDATA("testdata/ResourceFolderModel/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); + } +}; + +QTEST_GUILESS_MAIN(ResourceFolderModelTest) + +#include "ResourceFolderModel_test.moc" diff --git a/tests/ResourcePackParse_test.cpp b/tests/ResourcePackParse_test.cpp new file mode 100644 index 00000000..568c3b63 --- /dev/null +++ b/tests/ResourcePackParse_test.cpp @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <QTest> +#include <QTimer> + +#include <FileSystem.h> + +#include <minecraft/mod/ResourcePack.h> +#include <minecraft/mod/tasks/LocalResourcePackParseTask.h> + +class ResourcePackParseTest : public QObject { + Q_OBJECT + + private slots: + void test_parseZIP() + { + QString source = QFINDTESTDATA("testdata/ResourcePackParse"); + + QString zip_rp = FS::PathCombine(source, "test_resource_pack_idk.zip"); + ResourcePack pack { QFileInfo(zip_rp) }; + + ResourcePackUtils::processZIP(pack); + + QVERIFY(pack.packFormat() == 3); + QVERIFY(pack.description() == "um dois, feijão com arroz, três quatro, feijão no prato, cinco seis, café inglês, sete oito, comer biscoito, nove dez comer pastéis!!"); + } + + void test_parseFolder() + { + QString source = QFINDTESTDATA("testdata/ResourcePackParse"); + + QString folder_rp = FS::PathCombine(source, "test_folder"); + ResourcePack pack { QFileInfo(folder_rp) }; + + ResourcePackUtils::processFolder(pack); + + QVERIFY(pack.packFormat() == 1); + QVERIFY(pack.description() == "Some resource pack maybe"); + } + + void test_parseFolder2() + { + QString source = QFINDTESTDATA("testdata/ResourcePackParse"); + + QString folder_rp = FS::PathCombine(source, "another_test_folder"); + ResourcePack pack { QFileInfo(folder_rp) }; + + ResourcePackUtils::process(pack); + + QVERIFY(pack.packFormat() == 6); + QVERIFY(pack.description() == "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional"); + } +}; + +QTEST_GUILESS_MAIN(ResourcePackParseTest) + +#include "ResourcePackParse_test.moc" diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp new file mode 100644 index 00000000..80bba02f --- /dev/null +++ b/tests/Task_test.cpp @@ -0,0 +1,190 @@ +#include <QTest> + +#include <tasks/ConcurrentTask.h> +#include <tasks/MultipleOptionsTask.h> +#include <tasks/SequentialTask.h> +#include <tasks/Task.h> + +/* Does nothing. Only used for testing. */ +class BasicTask : public Task { + Q_OBJECT + + friend class TaskTest; + + private: + void executeTask() override + { + emitSucceeded(); + }; +}; + +/* Does nothing. Only used for testing. */ +class BasicTask_MultiStep : public Task { + Q_OBJECT + + friend class TaskTest; + + private: + auto isMultiStep() const -> bool override { return true; } + + void executeTask() override {}; +}; + +class TaskTest : public QObject { + Q_OBJECT + + private slots: + void test_SetStatus_NoMultiStep(){ + BasicTask t; + QString status {"test status"}; + + t.setStatus(status); + + QCOMPARE(t.getStatus(), status); + QCOMPARE(t.getStepStatus(), status); + } + + void test_SetStatus_MultiStep(){ + BasicTask_MultiStep t; + QString status {"test status"}; + + t.setStatus(status); + + QCOMPARE(t.getStatus(), status); + // Even though it is multi step, it does not override the getStepStatus method, + // so it should remain the same. + QCOMPARE(t.getStepStatus(), status); + } + + void test_SetProgress(){ + BasicTask t; + int current = 42; + int total = 207; + + t.setProgress(current, total); + + 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) + +#include "Task_test.moc" diff --git a/launcher/Version_test.cpp b/tests/Version_test.cpp index b2d657a6..734528b7 100644 --- a/launcher/Version_test.cpp +++ b/tests/Version_test.cpp @@ -15,7 +15,7 @@ #include <QTest> -#include "TestUtil.h" +#include <TestUtil.h> #include <Version.h> class ModUtilsTest : public QObject diff --git a/launcher/testdata/FileSystem-test_createShortcut-unix b/tests/testdata/FileSystem/FileSystem-test_createShortcut-unix index 1ce3a2bd..1ce3a2bd 100755 --- a/launcher/testdata/FileSystem-test_createShortcut-unix +++ b/tests/testdata/FileSystem/FileSystem-test_createShortcut-unix diff --git a/launcher/minecraft/mod/testdata/test_folder/assets/minecraft/textures/blah.txt b/tests/testdata/FileSystem/test_folder/assets/minecraft/textures/blah.txt index 8d1c8b69..8d1c8b69 100644 --- a/launcher/minecraft/mod/testdata/test_folder/assets/minecraft/textures/blah.txt +++ b/tests/testdata/FileSystem/test_folder/assets/minecraft/textures/blah.txt diff --git a/launcher/minecraft/mod/testdata/test_folder/pack.mcmeta b/tests/testdata/FileSystem/test_folder/pack.mcmeta index 67ee0434..67ee0434 100644 --- a/launcher/minecraft/mod/testdata/test_folder/pack.mcmeta +++ b/tests/testdata/FileSystem/test_folder/pack.mcmeta diff --git a/launcher/minecraft/mod/testdata/test_folder/pack.nfo b/tests/testdata/FileSystem/test_folder/pack.nfo index 8d1c8b69..8d1c8b69 100644 --- a/launcher/minecraft/mod/testdata/test_folder/pack.nfo +++ b/tests/testdata/FileSystem/test_folder/pack.nfo diff --git a/tests/testdata/Library b/tests/testdata/Library new file mode 120000 index 00000000..0e7a2286 --- /dev/null +++ b/tests/testdata/Library @@ -0,0 +1 @@ +MojangVersionFormat/
\ No newline at end of file diff --git a/launcher/minecraft/testdata/1.9-simple.json b/tests/testdata/MojangVersionFormat/1.9-simple.json index 574c5b06..574c5b06 100644 --- a/launcher/minecraft/testdata/1.9-simple.json +++ b/tests/testdata/MojangVersionFormat/1.9-simple.json diff --git a/launcher/minecraft/testdata/1.9.json b/tests/testdata/MojangVersionFormat/1.9.json index 697c6059..697c6059 100644 --- a/launcher/minecraft/testdata/1.9.json +++ b/tests/testdata/MojangVersionFormat/1.9.json diff --git a/launcher/minecraft/testdata/codecwav-20101023.jar b/tests/testdata/MojangVersionFormat/codecwav-20101023.jar index f5236083..f5236083 100644 --- a/launcher/minecraft/testdata/codecwav-20101023.jar +++ b/tests/testdata/MojangVersionFormat/codecwav-20101023.jar diff --git a/launcher/minecraft/testdata/lib-native-arch.json b/tests/testdata/MojangVersionFormat/lib-native-arch.json index 501826ae..501826ae 100644 --- a/launcher/minecraft/testdata/lib-native-arch.json +++ b/tests/testdata/MojangVersionFormat/lib-native-arch.json diff --git a/launcher/minecraft/testdata/lib-native.json b/tests/testdata/MojangVersionFormat/lib-native.json index 5b9f3b55..5b9f3b55 100644 --- a/launcher/minecraft/testdata/lib-native.json +++ b/tests/testdata/MojangVersionFormat/lib-native.json diff --git a/launcher/minecraft/testdata/lib-simple.json b/tests/testdata/MojangVersionFormat/lib-simple.json index 90bbff07..90bbff07 100644 --- a/launcher/minecraft/testdata/lib-simple.json +++ b/tests/testdata/MojangVersionFormat/lib-simple.json diff --git a/launcher/minecraft/testdata/testname-testversion-linux-32.jar b/tests/testdata/MojangVersionFormat/testname-testversion-linux-32.jar index f5236083..f5236083 100644 --- a/launcher/minecraft/testdata/testname-testversion-linux-32.jar +++ b/tests/testdata/MojangVersionFormat/testname-testversion-linux-32.jar diff --git a/launcher/mojang/testdata/1.8.0_202-x64.json b/tests/testdata/PackageManifest/1.8.0_202-x64.json index 3d99d719..3d99d719 100644 --- a/launcher/mojang/testdata/1.8.0_202-x64.json +++ b/tests/testdata/PackageManifest/1.8.0_202-x64.json diff --git a/launcher/mojang/testdata/inspect/a/b.txt b/tests/testdata/PackageManifest/inspect/a/b.txt index e69de29b..e69de29b 100755 --- a/launcher/mojang/testdata/inspect/a/b.txt +++ b/tests/testdata/PackageManifest/inspect/a/b.txt diff --git a/launcher/mojang/testdata/inspect/a/b/b.txt b/tests/testdata/PackageManifest/inspect/a/b/b.txt index 4e19a044..4e19a044 120000 --- a/launcher/mojang/testdata/inspect/a/b/b.txt +++ b/tests/testdata/PackageManifest/inspect/a/b/b.txt diff --git a/launcher/mojang/testdata/inspect_win/a/b.txt b/tests/testdata/PackageManifest/inspect_win/a/b.txt index e69de29b..e69de29b 100644 --- a/launcher/mojang/testdata/inspect_win/a/b.txt +++ b/tests/testdata/PackageManifest/inspect_win/a/b.txt diff --git a/launcher/mojang/testdata/inspect_win/a/b/b.txt b/tests/testdata/PackageManifest/inspect_win/a/b/b.txt index e69de29b..e69de29b 100644 --- a/launcher/mojang/testdata/inspect_win/a/b/b.txt +++ b/tests/testdata/PackageManifest/inspect_win/a/b/b.txt diff --git a/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml b/tests/testdata/Packwiz/borderless-mining.pw.toml index 16545fd4..16545fd4 100644 --- a/launcher/modplatform/packwiz/testdata/borderless-mining.pw.toml +++ b/tests/testdata/Packwiz/borderless-mining.pw.toml diff --git a/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml b/tests/testdata/Packwiz/screenshot-to-clipboard-fabric.pw.toml index 87d70ada..87d70ada 100644 --- a/launcher/modplatform/packwiz/testdata/screenshot-to-clipboard-fabric.pw.toml +++ b/tests/testdata/Packwiz/screenshot-to-clipboard-fabric.pw.toml diff --git a/tests/testdata/ResourceFolderModel b/tests/testdata/ResourceFolderModel new file mode 120000 index 00000000..c653d859 --- /dev/null +++ b/tests/testdata/ResourceFolderModel @@ -0,0 +1 @@ +ResourcePackParse
\ No newline at end of file diff --git a/tests/testdata/ResourcePackParse/another_test_folder/pack.mcmeta b/tests/testdata/ResourcePackParse/another_test_folder/pack.mcmeta new file mode 100644 index 00000000..d33a0e5d --- /dev/null +++ b/tests/testdata/ResourcePackParse/another_test_folder/pack.mcmeta @@ -0,0 +1,6 @@ +{
+ "pack": {
+ "pack_format": 6,
+ "description": "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional"
+ }
+}
diff --git a/tests/testdata/ResourcePackParse/supercoolmod.jar b/tests/testdata/ResourcePackParse/supercoolmod.jar new file mode 100644 index 00000000..d8cf9860 --- /dev/null +++ b/tests/testdata/ResourcePackParse/supercoolmod.jar @@ -0,0 +1 @@ +the best mod. diff --git a/launcher/testdata/test_folder/assets/minecraft/textures/blah.txt b/tests/testdata/ResourcePackParse/test_folder/assets/minecraft/textures/blah.txt index 8d1c8b69..8d1c8b69 100644 --- a/launcher/testdata/test_folder/assets/minecraft/textures/blah.txt +++ b/tests/testdata/ResourcePackParse/test_folder/assets/minecraft/textures/blah.txt diff --git a/launcher/testdata/test_folder/pack.mcmeta b/tests/testdata/ResourcePackParse/test_folder/pack.mcmeta index 67ee0434..67ee0434 100644 --- a/launcher/testdata/test_folder/pack.mcmeta +++ b/tests/testdata/ResourcePackParse/test_folder/pack.mcmeta diff --git a/launcher/testdata/test_folder/pack.nfo b/tests/testdata/ResourcePackParse/test_folder/pack.nfo index 8d1c8b69..8d1c8b69 100644 --- a/launcher/testdata/test_folder/pack.nfo +++ b/tests/testdata/ResourcePackParse/test_folder/pack.nfo diff --git a/tests/testdata/ResourcePackParse/test_resource_pack_idk.zip b/tests/testdata/ResourcePackParse/test_resource_pack_idk.zip Binary files differnew file mode 100644 index 00000000..52b91cdc --- /dev/null +++ b/tests/testdata/ResourcePackParse/test_resource_pack_idk.zip |