Merge remote-tracking branch 'upstream/develop' into develop

Signed-off-by: sneedium <sneed@sneedmc.org>
This commit is contained in:
sneedium 2022-10-18 20:02:47 -04:00
commit e7581535cd
Signed by: sneedium
GPG Key ID: 906F66490FBE722F
355 changed files with 9951 additions and 14212 deletions

View File

@ -27,7 +27,14 @@ body:
attributes: attributes:
label: Version of PolyMC label: Version of PolyMC
description: The version of PolyMC used in the bug report. 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: validations:
required: true required: true
- type: textarea - type: textarea

3
.github/codeql/codeql-config.yml vendored Normal file
View File

@ -0,0 +1,3 @@
query-filters:
- exclude:
id: cpp/fixme-comment

View File

@ -27,7 +27,6 @@ jobs:
qt_host: linux qt_host: linux
qt_version: '6.2.4' qt_version: '6.2.4'
qt_modules: 'qt5compat qtimageformats' qt_modules: 'qt5compat qtimageformats'
qt_path: /home/runner/work/PolyMC/Qt
- os: windows-2022 - os: windows-2022
name: "Windows-Legacy" name: "Windows-Legacy"
@ -40,12 +39,20 @@ jobs:
qt_ver: 6 qt_ver: 6
- os: macos-12 - os: macos-12
macosx_deployment_target: 10.14 name: macOS
macosx_deployment_target: 10.15
qt_ver: 6 qt_ver: 6
qt_host: mac qt_host: mac
qt_version: '6.3.1' qt_version: '6.3.0'
qt_modules: 'qt5compat qtimageformats' qt_modules: 'qt5compat qtimageformats'
qt_path: /Users/runner/work/PolyMC/Qt
- os: macos-12
name: macOS-Legacy
macosx_deployment_target: 10.13
qt_ver: 5
qt_host: mac
qt_version: '5.15.2'
qt_modules: ''
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -66,6 +73,14 @@ jobs:
with: with:
submodules: 'true' submodules: 'true'
- name: Initialize CodeQL
if: runner.os == 'Linux' && matrix.qt_ver == 6
uses: github/codeql-action/init@v2
with:
config-file: ./.github/codeql/codeql-config.yml
queries: security-and-quality
languages: cpp, java
- name: 'Setup MSYS2' - name: 'Setup MSYS2'
if: runner.os == 'Windows' if: runner.os == 'Windows'
uses: msys2/setup-msys2@v2 uses: msys2/setup-msys2@v2
@ -141,24 +156,16 @@ jobs:
run: | run: |
sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 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) - name: Install Qt (macOS and AppImage)
if: matrix.qt_ver == 6 && runner.os != 'Windows' if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS'
uses: jurplel/install-qt-action@v2 uses: jurplel/install-qt-action@v3
with: with:
version: ${{ matrix.qt_version }} version: ${{ matrix.qt_version }}
host: ${{ matrix.qt_host }} host: ${{ matrix.qt_host }}
target: 'desktop' target: 'desktop'
modules: ${{ matrix.qt_modules }} modules: ${{ matrix.qt_modules }}
cached: ${{ steps.cache-qt.outputs.cache-hit }} cache: true
aqtversion: ==2.1.* cache-key-prefix: ${{ matrix.qt_host }}-${{ matrix.qt_version }}-"${{ matrix.qt_modules }}"-qt_cache
- name: Prepare AppImage (Linux) - name: Prepare AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5 if: runner.os == 'Linux' && matrix.qt_ver != 5
@ -174,9 +181,14 @@ jobs:
## ##
- name: Configure CMake (macOS) - name: Configure CMake (macOS)
if: runner.os == 'macOS' if: runner.os == 'macOS' && matrix.qt_ver == 6
run: | run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -DLauncher_BUILD_PLATFORM=macOS -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -G Ninja cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja
- name: Configure CMake (macOS-Legacy)
if: runner.os == 'macOS' && matrix.qt_ver == 5
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -G Ninja
- name: Configure CMake (Windows) - name: Configure CMake (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'
@ -219,6 +231,14 @@ jobs:
run: | run: |
ctest --test-dir build --output-on-failure ctest --test-dir build --output-on-failure
##
# CODE SCAN
##
- name: Perform CodeQL Analysis
if: runner.os == 'Linux' && matrix.qt_ver == 6
uses: github/codeql-action/analyze@v2
## ##
# PACKAGE BUILDS # PACKAGE BUILDS
## ##
@ -234,7 +254,7 @@ jobs:
tar -czf ../PolyMC.tar.gz * tar -czf ../PolyMC.tar.gz *
- name: Make Sparkle signature (macOS) - name: Make Sparkle signature (macOS)
if: runner.os == 'macOS' if: matrix.name == 'macOS'
run: | run: |
if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then
brew install openssl@3 brew install openssl@3
@ -259,10 +279,8 @@ jobs:
cmake --install ${{ env.BUILD_DIR }} cmake --install ${{ env.BUILD_DIR }}
cd ${{ env.INSTALL_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 ./ 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 fi
- name: Package (Windows, portable) - name: Package (Windows, portable)
@ -315,6 +333,9 @@ jobs:
cp -r /home/runner/work/PolyMC/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines 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"
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server" LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server"
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64" LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64"
@ -332,7 +353,7 @@ jobs:
if: runner.os == 'macOS' if: runner.os == 'macOS'
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} name: PolyMC-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
path: PolyMC.tar.gz path: PolyMC.tar.gz
- name: Upload binary zip (Windows) - name: Upload binary zip (Windows)

View File

@ -11,6 +11,7 @@ on:
- '**.nix' - '**.nix'
- 'packages/**' - 'packages/**'
- '.github/ISSUE_TEMPLATE/**' - '.github/ISSUE_TEMPLATE/**'
- '.markdownlint**'
pull_request: pull_request:
paths-ignore: paths-ignore:
- '**.md' - '**.md'
@ -19,6 +20,7 @@ on:
- '**.nix' - '**.nix'
- 'packages/**' - 'packages/**'
- '.github/ISSUE_TEMPLATE/**' - '.github/ISSUE_TEMPLATE/**'
- '.markdownlint**'
workflow_dispatch: workflow_dispatch:
jobs: jobs:

View File

@ -40,6 +40,7 @@ jobs:
mv PolyMC-Linux-Portable*/PolyMC-portable.tar.gz PolyMC-Linux-Portable-${{ env.VERSION }}.tar.gz mv PolyMC-Linux-Portable*/PolyMC-portable.tar.gz PolyMC-Linux-Portable-${{ env.VERSION }}.tar.gz
mv PolyMC-Linux*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz mv PolyMC-Linux*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz
mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage
mv PolyMC-macOS-Legacy*/PolyMC.tar.gz PolyMC-macOS-Legacy-${{ env.VERSION }}.tar.gz
mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz
tar -czf PolyMC-${{ env.VERSION }}.tar.gz PolyMC-${{ env.VERSION }} tar -czf PolyMC-${{ env.VERSION }}.tar.gz PolyMC-${{ env.VERSION }}
@ -80,4 +81,5 @@ jobs:
PolyMC-Windows-Portable-${{ env.VERSION }}.zip PolyMC-Windows-Portable-${{ env.VERSION }}.zip
PolyMC-Windows-Setup-${{ env.VERSION }}.exe PolyMC-Windows-Setup-${{ env.VERSION }}.exe
PolyMC-macOS-${{ env.VERSION }}.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz
PolyMC-macOS-Legacy-${{ env.VERSION }}.tar.gz
PolyMC-${{ env.VERSION }}.tar.gz PolyMC-${{ env.VERSION }}.tar.gz

View File

@ -7,8 +7,9 @@ jobs:
publish: publish:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: vedantmgoyal2009/winget-releaser@latest - uses: vedantmgoyal2009/winget-releaser@v1
with: with:
identifier: PolyMC.PolyMC identifier: PolyMC.PolyMC
installers-regex: '\.exe$' version: ${{ github.event.release.tag_name }}
installers-regex: 'PolyMC-Windows-Setup-.+\.exe$'
token: ${{ secrets.WINGET_TOKEN }} token: ${{ secrets.WINGET_TOKEN }}

8
.gitmodules vendored
View File

@ -1,8 +1,12 @@
[submodule "depends/libnbtplusplus"] [submodule "depends/libnbtplusplus"]
path = libraries/libnbtplusplus path = libraries/libnbtplusplus
url = https://github.com/PolyMC/libnbtplusplus.git url = https://github.com/PolyMC/libnbtplusplus.git
pushurl = git@github.com:PolyMC/libnbtplusplus.git
[submodule "libraries/quazip"] [submodule "libraries/quazip"]
path = libraries/quazip path = libraries/quazip
url = https://github.com/stachenov/quazip.git url = https://github.com/stachenov/quazip.git
[submodule "libraries/tomlplusplus"]
path = libraries/tomlplusplus
url = https://github.com/marzer/tomlplusplus.git
[submodule "libraries/filesystem"]
path = libraries/filesystem
url = https://github.com/gulrak/filesystem

12
.markdownlint.yaml Normal file
View File

@ -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

2
.markdownlintignore Normal file
View File

@ -0,0 +1,2 @@
libraries/nbtplusplus
libraries/quazip

View File

@ -29,13 +29,10 @@ set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${PROJECT_BINARY_DIR}/jars)
######## Set compiler flags ######## ######## Set compiler flags ########
set(CMAKE_CXX_STANDARD_REQUIRED true) set(CMAKE_CXX_STANDARD_REQUIRED true)
set(CMAKE_C_STANDARD_REQUIRED true) set(CMAKE_C_STANDARD_REQUIRED true)
set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD 11)
include(GenerateExportHeader) 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()
# Fix build with Qt 5.13 # Fix build with Qt 5.13
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y")
@ -79,12 +76,12 @@ set(Launcher_NEWS_OPEN_URL "https://multimc.org/posts.html" CACHE STRING "URL th
set(Launcher_HELP_URL "" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") set(Launcher_HELP_URL "" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help")
######## Set version numbers ######## ######## Set version numbers ########
set(Launcher_VERSION_MAJOR 1) set(Launcher_VERSION_MAJOR 5)
set(Launcher_VERSION_MINOR 4) set(Launcher_VERSION_MINOR 0)
set(Launcher_VERSION_HOTFIX 0)
# Build number set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") 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. # 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.") 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.")
@ -132,15 +129,8 @@ message(STATUS "Git commit: ${Launcher_GIT_COMMIT}")
message(STATUS "Git tag: ${Launcher_GIT_TAG}") message(STATUS "Git tag: ${Launcher_GIT_TAG}")
message(STATUS "Git refspec: ${Launcher_GIT_REFSPEC}") 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") string(TIMESTAMP TODAY "%Y-%m-%d")
set(Launcher_RELEASE_TIMESTAMP "${TODAY}") set(Launcher_BUILD_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}\\']")
################################ 3rd Party Libs ################################ ################################ 3rd Party Libs ################################
@ -188,6 +178,14 @@ if (Qt5_POSITION_INDEPENDENT_CODE)
SET(CMAKE_POSITION_INDEPENDENT_CODE ON) SET(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif() endif()
if(NOT Launcher_FORCE_BUNDLED_LIBS)
# Find toml++
find_package(tomlplusplus 3.2.0 QUIET)
# Find ghc_filesystem
find_package(ghc_filesystem QUIET)
endif()
####################################### Program Info ####################################### ####################################### Program Info #######################################
set(Launcher_APP_BINARY_NAME "sneedmc" CACHE STRING "Name of the Launcher binary") set(Launcher_APP_BINARY_NAME "sneedmc" CACHE STRING "Name of the Launcher binary")
@ -296,7 +294,6 @@ add_subdirectory(libraries/systeminfo) # system information library
add_subdirectory(libraries/hoedown) # markdown parser add_subdirectory(libraries/hoedown) # markdown parser
add_subdirectory(libraries/launcher) # java based launcher part for Minecraft add_subdirectory(libraries/launcher) # java based launcher part for Minecraft
add_subdirectory(libraries/javacheck) # java compatibility checker add_subdirectory(libraries/javacheck) # java compatibility checker
add_subdirectory(libraries/xz-embedded) # xz compression
if (FORCE_BUNDLED_QUAZIP) if (FORCE_BUNDLED_QUAZIP)
message(STATUS "Using bundled QuaZip") message(STATUS "Using bundled QuaZip")
set(BUILD_SHARED_LIBS 0) # link statically to avoid conflicts. set(BUILD_SHARED_LIBS 0) # link statically to avoid conflicts.
@ -307,16 +304,30 @@ else()
endif() endif()
add_subdirectory(libraries/rainbow) # Qt extension for colors add_subdirectory(libraries/rainbow) # Qt extension for colors
add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions
add_subdirectory(libraries/classparser) # class parser library if(NOT tomlplusplus_FOUND)
add_subdirectory(libraries/optional-bare) message(STATUS "Using bundled tomlplusplus")
add_subdirectory(libraries/tomlc99) # toml parser add_subdirectory(libraries/tomlplusplus) # toml parser
else()
message(STATUS "Using system tomlplusplus")
endif()
add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much
add_subdirectory(libraries/gamemode) add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
if (NOT ghc_filesystem_FOUND)
message(STATUS "Using bundled ghc_filesystem")
set(GHC_FILESYSTEM_WITH_INSTALL OFF) # Workaround ghc::filesystem bug
add_subdirectory(libraries/filesystem) # Implementation of std::filesystem for old C++, for usage in old macOS
add_library(ghcFilesystem::ghc_filesystem ALIAS ghc_filesystem)
else()
message(STATUS "Using system ghc_filesystem")
endif()
############################### Built Artifacts ############################### ############################### Built Artifacts ###############################
add_subdirectory(buildconfig) 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. # NOTE: this must always be last to appease the CMake deity of quirky install command evaluation order.
add_subdirectory(launcher) add_subdirectory(launcher)

View File

@ -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. If there is no existing formatting, you may use `clang-format` with our included `.clang-format` configuration.
In general, in order of importance: In general, in order of importance:
- Make sure your IDE is not messing up line endings or whitespace and avoid using linters. - Make sure your IDE is not messing up line endings or whitespace and avoid using linters.
- Prefer readability over dogma. - Prefer readability over dogma.
- Keep to the existing formatting. - Keep to the existing formatting.
@ -26,6 +27,7 @@ Signed-off-by: Author name <Author email>
By signing off your work, you agree to the terms below: By signing off your work, you agree to the terms below:
```
Developer's Certificate of Origin 1.1 Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that: By making a contribution to this project, I certify that:
@ -51,12 +53,11 @@ By signing off your work, you agree to the terms below:
personal information I submit with it, including my sign-off) is personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved. 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. 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. 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-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 [gh-vigilant-mode]: https://docs.github.com/en/authentication/managing-commit-signature-verification/displaying-verification-statuses-for-all-of-your-commits

View File

@ -1,4 +1,4 @@
# PolyMC ## PolyMC
PolyMC - Minecraft Launcher PolyMC - Minecraft Launcher
Copyright (C) 2021-2022 PolyMC Contributors Copyright (C) 2021-2022 PolyMC Contributors
@ -32,36 +32,56 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. 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 This license has been certified as open source. It has also been designated
copy of this software and associated documentation files (the "Software"), as GPL compatible by the Free Software Foundation (FSF).
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice, this permission notice and the below disclaimer Redistribution and use in source and binary forms, with or without
shall be included in all copies or substantial portions of the Software. modification, are permitted provided that the following conditions are met:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1. Redistributions in source code must retain the accompanying copyright
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, notice, this list of conditions, and the following disclaimer.
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 2. Redistributions in binary form must reproduce the accompanying
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER copyright notice, this list of conditions, and the following disclaimer
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING in the documentation and/or other materials provided with the
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER distribution.
DEALINGS IN THE SOFTWARE. 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.
# Qt 5/6 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. Copyright (C) 2022 The Qt Company Ltd and other contributors.
Contact: https://www.qt.io/licensing Contact: https://www.qt.io/licensing
Licensed under LGPL v3 Licensed under LGPL v3
# libnbt++ ## libnbt++
libnbt++ - A library for the Minecraft Named Binary Tag format. libnbt++ - A library for the Minecraft Named Binary Tag format.
Copyright (C) 2013, 2015 ljfa-ag Copyright (C) 2013, 2015 ljfa-ag
@ -79,7 +99,7 @@
You should have received a copy of the GNU Lesser General Public License You should have received a copy of the GNU Lesser General Public License
along with libnbt++. If not, see <http://www.gnu.org/licenses/>. 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 Matthew Woehlke <mw_triad@users.sourceforge.net>
Copyright (C) 2007 Olaf Schmidt <ojschmidt@kde.org> Copyright (C) 2007 Olaf Schmidt <ojschmidt@kde.org>
@ -102,7 +122,7 @@
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA. Boston, MA 02110-1301, USA.
# Hoedown ## Hoedown
Copyright (c) 2008, Natacha Porté Copyright (c) 2008, Natacha Porté
Copyright (c) 2011, Vicent Martí Copyright (c) 2011, Vicent Martí
@ -120,7 +140,7 @@
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 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") 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 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, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THE USE OF THE ICONS,
EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 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/), Copyright (c) 2014, Austin Andrews (http://materialdesignicons.com/),
with Reserved Font Name Material Design Icons. with Reserved Font Name Material Design Icons.
@ -147,7 +167,7 @@
This license is copied below, and is also available with a FAQ at: This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL http://scripts.sil.org/OFL
# Quazip ## Quazip
Copyright (C) 2005-2021 Sergey A. Tachenov Copyright (C) 2005-2021 Sergey A. Tachenov
@ -171,7 +191,7 @@
See COPYING file for the full LGPL text. See COPYING file for the full LGPL text.
# xz-minidec ## xz-minidec
XZ decompressor XZ decompressor
@ -181,7 +201,7 @@
This file has been put into the public domain. This file has been put into the public domain.
You can do whatever you want with this file. You can do whatever you want with this file.
# ColumnResizer ## ColumnResizer
Copyright (c) 2011-2016 Aurélien Gâteau and contributors. 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 OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# launcher (`libraries/launcher`) ## launcher (`libraries/launcher`)
PolyMC - Minecraft Launcher PolyMC - Minecraft Launcher
Copyright (C) 2021-2022 PolyMC Contributors Copyright (C) 2021-2022 PolyMC Contributors
@ -268,7 +288,7 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
# lionshead ## lionshead
Code has been taken from https://github.com/natefoo/lionshead and loosely Code has been taken from https://github.com/natefoo/lionshead and loosely
translated to C++ laced with Qt. translated to C++ laced with Qt.
@ -295,60 +315,26 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
# optional-bare ## tomlplusplus
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
MIT License MIT License
Copyright (c) 2017 CK Tan Copyright (c) Mark Gillard <mark.gillard@outlook.com.au>
https://github.com/cktan/tomlc99
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
of this software and associated documentation files (the "Software"), to deal documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
in the Software without restriction, including without limitation the rights rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell permit persons to whom the Software is furnished to do so, subject to the following conditions:
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
copies or substantial portions of the Software. Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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.
# O2 (Katabasis fork) ## O2 (Katabasis fork)
Copyright (c) 2012, Akos Polster Copyright (c) 2012, Akos Polster
All rights reserved. All rights reserved.
@ -373,3 +359,32 @@
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 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 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. 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.

View File

@ -167,11 +167,12 @@ The translation effort for SneedMC is hosted on [Weblate](https://hosted.weblate
## Download information ## Download information
To modify download information or change packaging information send a pull request or issue to the website [Here](https://github.com/PolyMC/polymc.github.io/blob/master/src/download.md) To modify download information or change packaging information send a pull request or issue to the website [here](https://github.com/PolyMC/polymc.github.io/tree/master/src/download).
## Forking/Redistributing/Custom builds policy ## Forking/Redistributing/Custom builds policy
SNEED SNEED
## Sponsors ## Sponsors
Money is not sneedful Money is not sneedful

View File

@ -55,18 +55,9 @@ Config::Config()
// Version information // Version information
VERSION_MAJOR = @Launcher_VERSION_MAJOR@; VERSION_MAJOR = @Launcher_VERSION_MAJOR@;
VERSION_MINOR = @Launcher_VERSION_MINOR@; VERSION_MINOR = @Launcher_VERSION_MINOR@;
VERSION_HOTFIX = @Launcher_VERSION_HOTFIX@;
VERSION_BUILD = @Launcher_VERSION_BUILD@;
BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@";
BUILD_DATE = "@Launcher_BUILD_TIMESTAMP@";
MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@";
MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@";
if (BUILD_PLATFORM == "macOS" && !MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty())
{
UPDATER_ENABLED = true;
}
GIT_COMMIT = "@Launcher_GIT_COMMIT@"; GIT_COMMIT = "@Launcher_GIT_COMMIT@";
GIT_TAG = "@Launcher_GIT_TAG@"; GIT_TAG = "@Launcher_GIT_TAG@";
@ -94,7 +85,6 @@ Config::Config()
VERSION_CHANNEL = "unknown"; VERSION_CHANNEL = "unknown";
} }
VERSION_STR = "@Launcher_VERSION_STRING@";
NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@"; NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@";
NEWS_OPEN_URL = "@Launcher_NEWS_OPEN_URL@"; NEWS_OPEN_URL = "@Launcher_NEWS_OPEN_URL@";
HELP_URL = "@Launcher_HELP_URL@"; HELP_URL = "@Launcher_HELP_URL@";
@ -112,7 +102,7 @@ Config::Config()
QString Config::versionString() const 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 QString Config::printableVersionString() const
@ -124,11 +114,5 @@ QString Config::printableVersionString() const
{ {
vstr += "-" + VERSION_CHANNEL; 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; return vstr;
} }

View File

@ -55,14 +55,13 @@ class Config {
int VERSION_MAJOR; int VERSION_MAJOR;
/// The minor version number. /// The minor version number.
int VERSION_MINOR; int VERSION_MINOR;
/// The hotfix number.
int VERSION_HOTFIX;
/// The build number.
int VERSION_BUILD;
/// A short string identifying this build's platform. For example, "lin64" or "win32". /// A short string identifying this build's platform. For example, "lin64" or "win32".
QString BUILD_PLATFORM; QString BUILD_PLATFORM;
/// A string containing the build timestamp
QString BUILD_DATE;
/// User-Agent to use. /// User-Agent to use.
QString USER_AGENT; QString USER_AGENT;
@ -80,9 +79,6 @@ class Config {
/// The git refspec of this build /// The git refspec of this build
QString GIT_REFSPEC; QString GIT_REFSPEC;
/// This is printed on start to standard output
QString VERSION_STR;
/** /**
* This is used to fetch the news RSS feed. * This is used to fetch the news RSS feed.
* It defaults in CMakeLists.txt to "https://multimc.org/rss.xml" * It defaults in CMakeLists.txt to "https://multimc.org/rss.xml"

View File

@ -52,7 +52,24 @@
"inputs": { "inputs": {
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"libnbtplusplus": "libnbtplusplus", "libnbtplusplus": "libnbtplusplus",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs",
"tomlplusplus": "tomlplusplus"
}
},
"tomlplusplus": {
"flake": false,
"locked": {
"lastModified": 1664034574,
"narHash": "sha256-EFMAl6tsTvkgK0DWC/pZfOIq06b2e5SnxJa1ngGRIQA=",
"owner": "marzer",
"repo": "tomlplusplus",
"rev": "8aa5c8b2a4ff2c440d4630addf64fa4f62146170",
"type": "github"
},
"original": {
"owner": "marzer",
"repo": "tomlplusplus",
"type": "github"
} }
} }
}, },

View File

@ -5,9 +5,10 @@
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; flake-compat = { url = "github:edolstra/flake-compat"; flake = false; };
libnbtplusplus = { url = "github:PolyMC/libnbtplusplus"; flake = false; }; libnbtplusplus = { url = "github:PolyMC/libnbtplusplus"; flake = false; };
tomlplusplus = { url = "github:marzer/tomlplusplus"; flake = false; };
}; };
outputs = { self, nixpkgs, libnbtplusplus, ... }: outputs = { self, nixpkgs, libnbtplusplus, tomlplusplus, ... }:
let let
# User-friendly version number. # User-friendly version number.
version = builtins.substring 0 8 self.lastModifiedDate; version = builtins.substring 0 8 self.lastModifiedDate;
@ -22,8 +23,8 @@
pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system}); pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system});
packagesFn = pkgs: rec { packagesFn = pkgs: rec {
polymc = pkgs.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus; }; polymc = pkgs.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus tomlplusplus; };
polymc-qt6 = pkgs.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus; }; polymc-qt6 = pkgs.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus tomlplusplus; };
}; };
in in
{ {

View File

@ -60,6 +60,10 @@
#include "ui/themes/BrightTheme.h" #include "ui/themes/BrightTheme.h"
#include "ui/themes/CustomTheme.h" #include "ui/themes/CustomTheme.h"
#ifdef Q_OS_WIN
#include "ui/WinDarkmode.h"
#endif
#include "ui/setupwizard/SetupWizard.h" #include "ui/setupwizard/SetupWizard.h"
#include "ui/setupwizard/LanguageWizardPage.h" #include "ui/setupwizard/LanguageWizardPage.h"
#include "ui/setupwizard/JavaWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h"
@ -74,6 +78,7 @@
#include <iostream> #include <iostream>
#include <QAccessible> #include <QAccessible>
#include <QCommandLineParser>
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
@ -104,13 +109,17 @@
#include "translations/TranslationsModel.h" #include "translations/TranslationsModel.h"
#include "meta/Index.h" #include "meta/Index.h"
#include <Commandline.h>
#include <FileSystem.h> #include <FileSystem.h>
#include <DesktopServices.h> #include <DesktopServices.h>
#include <LocalPeer.h> #include <LocalPeer.h>
#include <sys.h> #include <sys.h>
#ifdef Q_OS_LINUX
#include <dlfcn.h>
#include "gamemode_client.h"
#endif
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
#ifndef WIN32_LEAN_AND_MEAN #ifndef WIN32_LEAN_AND_MEAN
@ -125,12 +134,6 @@
static const QLatin1String liveCheckFile("live.check"); static const QLatin1String liveCheckFile("live.check");
using namespace Commandline;
#define MACOS_HINT "If you are on macOS Sierra, you might have to move the app to your /Applications or ~/Applications folder. "\
"This usually fixes the problem and you can move the application elsewhere afterwards.\n"\
"\n"
namespace { namespace {
void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{ {
@ -192,80 +195,27 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
this->setQuitOnLastWindowClosed(false); this->setQuitOnLastWindowClosed(false);
// Commandline parsing // Commandline parsing
QHash<QString, QVariant> args; QCommandLineParser parser;
{ parser.setApplicationDescription(BuildConfig.LAUNCHER_NAME);
Parser parser(FlagStyle::GNU, ArgumentStyle::SpaceAndEquals);
// --help parser.addOptions({
parser.addSwitch("help"); {{"d", "dir"}, "Use a custom path as application root (use '.' for current directory)", "directory"},
parser.addShortOpt("help", 'h'); {{"l", "launch"}, "Launch the specified instance (by instance ID)", "instance"},
parser.addDocumentation("help", "Display this help and exit."); {{"s", "server"}, "Join the specified server on launch (only valid in combination with --launch)", "address"},
// --version {{"a", "profile"}, "Use the account specified by its profile name (only valid in combination with --launch)", "profile"},
parser.addSwitch("version"); {"alive", "Write a small '" + liveCheckFile + "' file after the launcher starts"},
parser.addShortOpt("version", 'V'); {{"I", "import"}, "Import instance from specified zip (local path or URL)", "file"}
parser.addDocumentation("version", "Display program version and exit."); });
// --dir parser.addHelpOption();
parser.addOption("dir"); parser.addVersionOption();
parser.addShortOpt("dir", 'd');
parser.addDocumentation("dir", "Use the supplied folder as application root instead of the binary location (use '.' for current)");
// --launch
parser.addOption("launch");
parser.addShortOpt("launch", 'l');
parser.addDocumentation("launch", "Launch the specified instance (by instance ID)");
// --server
parser.addOption("server");
parser.addShortOpt("server", 's');
parser.addDocumentation("server", "Join the specified server on launch (only valid in combination with --launch)");
// --profile
parser.addOption("profile");
parser.addShortOpt("profile", 'a');
parser.addDocumentation("profile", "Use the account specified by its profile name (only valid in combination with --launch)");
// --alive
parser.addSwitch("alive");
parser.addDocumentation("alive", "Write a small '" + liveCheckFile + "' file after the launcher starts");
// --import
parser.addOption("import");
parser.addShortOpt("import", 'I');
parser.addDocumentation("import", "Import instance from specified zip (local path or URL)");
// parse the arguments parser.process(arguments());
try
{
args = parser.parse(arguments());
}
catch (const ParsingError &e)
{
std::cerr << "CommandLineError: " << e.what() << std::endl;
if(argc > 0)
std::cerr << "Try '" << argv[0] << " -h' to get help on command line parameters."
<< std::endl;
m_status = Application::Failed;
return;
}
// display help and exit m_instanceIdToLaunch = parser.value("launch");
if (args["help"].toBool()) m_serverToJoin = parser.value("server");
{ m_profileToUse = parser.value("profile");
std::cout << qPrintable(parser.compileHelp(arguments()[0])); m_liveCheck = parser.isSet("alive");
m_status = Application::Succeeded; m_zipToImport = parser.value("import");
return;
}
// display version and exit
if (args["version"].toBool())
{
std::cout << "Version " << BuildConfig.printableVersionString().toStdString() << std::endl;
std::cout << "Git " << BuildConfig.GIT_COMMIT.toStdString() << std::endl;
m_status = Application::Succeeded;
return;
}
}
m_instanceIdToLaunch = args["launch"].toString();
m_serverToJoin = args["server"].toString();
m_profileToUse = args["profile"].toString();
m_liveCheck = args["alive"].toBool();
m_zipToImport = args["import"].toUrl();
// error if --launch is missing with --server or --profile // error if --launch is missing with --server or --profile
if((!m_serverToJoin.isEmpty() || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty()) if((!m_serverToJoin.isEmpty() || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty())
@ -280,7 +230,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
{ {
// Root path is used for updates and portable data // 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 QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr
m_rootPath = foo.absolutePath(); m_rootPath = foo.absolutePath();
#elif defined(Q_OS_WIN32) #elif defined(Q_OS_WIN32)
@ -296,7 +246,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
QString adjustedBy; QString adjustedBy;
QString dataPath; QString dataPath;
// change folder // change folder
QString dirParam = args["dir"].toString(); QString dirParam = parser.value("dir");
if (!dirParam.isEmpty()) if (!dirParam.isEmpty())
{ {
// the dir param. it makes multimc data path point to whatever the user specified // the dir param. it makes multimc data path point to whatever the user specified
@ -343,9 +293,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
QString( QString(
"The launcher data folder could not be created.\n" "The launcher data folder could not be created.\n"
"\n" "\n"
#if defined(Q_OS_MAC)
MACOS_HINT
#endif
"Make sure you have the right permissions to the launcher data folder and any folder needed to access it.\n" "Make sure you have the right permissions to the launcher data folder and any folder needed to access it.\n"
"(%1)\n" "(%1)\n"
"\n" "\n"
@ -361,9 +308,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
QString( QString(
"The launcher data folder could not be opened.\n" "The launcher data folder could not be opened.\n"
"\n" "\n"
#if defined(Q_OS_MAC)
MACOS_HINT
#endif
"Make sure you have the right permissions to the launcher data folder.\n" "Make sure you have the right permissions to the launcher data folder.\n"
"(%1)\n" "(%1)\n"
"\n" "\n"
@ -444,9 +388,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
QString( QString(
"The launcher couldn't create a log file - the data folder is not writable.\n" "The launcher couldn't create a log file - the data folder is not writable.\n"
"\n" "\n"
#if defined(Q_OS_MAC)
MACOS_HINT
#endif
"Make sure you have write permissions to the data folder.\n" "Make sure you have write permissions to the data folder.\n"
"(%1)\n" "(%1)\n"
"\n" "\n"
@ -509,13 +450,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// Initialize application settings // Initialize application settings
{ {
m_settings.reset(new INISettingsObject(BuildConfig.LAUNCHER_CONFIGFILE, this)); m_settings.reset(new INISettingsObject(BuildConfig.LAUNCHER_CONFIGFILE, this));
<<<<<<< HEAD
=======
// Updates
// Multiple channels are separated by spaces
m_settings->registerSetting("UpdateChannel", BuildConfig.VERSION_CHANNEL);
m_settings->registerSetting("AutoUpdate", true);
>>>>>>> upstream/develop
// Theming // Theming
m_settings->registerSetting("IconTheme", QString("pe_colored")); m_settings->registerSetting("IconTheme", QString("pe_colored"));
@ -650,6 +584,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("UpdateDialogGeometry", ""); m_settings->registerSetting("UpdateDialogGeometry", "");
m_settings->registerSetting("ModDownloadGeometry", "");
// HACK: This code feels so stupid is there a less stupid way of doing this? // HACK: This code feels so stupid is there a less stupid way of doing this?
{ {
m_settings->registerSetting("PastebinURL", ""); m_settings->registerSetting("PastebinURL", "");
@ -738,19 +674,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
qDebug() << "<> Translations loaded."; qDebug() << "<> Translations loaded.";
} }
<<<<<<< HEAD
=======
// initialize the updater
if(BuildConfig.UPDATER_ENABLED)
{
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));
qDebug() << "<> Updater started.";
}
>>>>>>> upstream/develop
// Instance icons // Instance icons
{ {
auto setting = APPLICATION->settings()->getSetting("IconsDir"); auto setting = APPLICATION->settings()->getSetting("IconsDir");
@ -837,6 +760,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath()); m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath());
m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath()); m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());
m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").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("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath());
m_metacache->addBase("root", QDir::currentPath()); m_metacache->addBase("root", QDir::currentPath());
m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("translations", QDir("translations").absolutePath());
@ -888,10 +812,13 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
qDebug() << "<> Application theme set."; qDebug() << "<> Application theme set.";
} }
updateCapabilities();
if(createSetupWizard()) if(createSetupWizard())
{ {
return; return;
} }
performMainStartupAction(); performMainStartupAction();
} }
@ -1000,7 +927,7 @@ void Application::performMainStartupAction()
qDebug() << " Launching with account" << m_profileToUse; qDebug() << " Launching with account" << m_profileToUse;
} }
launch(inst, true, nullptr, serverToJoin, accountToUse); launch(inst, true, false, nullptr, serverToJoin, accountToUse);
return; return;
} }
} }
@ -1104,6 +1031,7 @@ void Application::messageReceived(const QByteArray& message)
launch( launch(
instance, instance,
true, true,
false,
nullptr, nullptr,
serverObject, serverObject,
accountObject accountObject
@ -1158,6 +1086,15 @@ void Application::setApplicationTheme(const QString& name, bool initial)
{ {
auto & theme = (*themeIter).second; auto & theme = (*themeIter).second;
theme->apply(initial); theme->apply(initial);
#ifdef Q_OS_WIN
if (m_mainWindow) {
if (QString::compare(theme->id(), "dark") == 0) {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true);
} else {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false);
}
}
#endif
} }
else else
{ {
@ -1195,6 +1132,7 @@ bool Application::openJsonEditor(const QString &filename)
bool Application::launch( bool Application::launch(
InstancePtr instance, InstancePtr instance,
bool online, bool online,
bool demo,
BaseProfilerFactory *profiler, BaseProfilerFactory *profiler,
MinecraftServerTargetPtr serverToJoin, MinecraftServerTargetPtr serverToJoin,
MinecraftAccountPtr accountToUse MinecraftAccountPtr accountToUse
@ -1214,6 +1152,7 @@ bool Application::launch(
controller.reset(new LaunchController()); controller.reset(new LaunchController());
controller->setInstance(instance); controller->setInstance(instance);
controller->setOnline(online); controller->setOnline(online);
controller->setDemo(demo);
controller->setProfiler(profiler); controller->setProfiler(profiler);
controller->setServerToJoin(serverToJoin); controller->setServerToJoin(serverToJoin);
controller->setAccountToUse(accountToUse); controller->setAccountToUse(accountToUse);
@ -1227,6 +1166,9 @@ bool Application::launch(
} }
connect(controller.get(), &LaunchController::succeeded, this, &Application::controllerSucceeded); connect(controller.get(), &LaunchController::succeeded, this, &Application::controllerSucceeded);
connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed); connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed);
connect(controller.get(), &LaunchController::aborted, this, [this] {
controllerFailed(tr("Aborted"));
});
addRunningInstance(); addRunningInstance();
controller->start(); controller->start();
return true; return true;
@ -1362,6 +1304,13 @@ MainWindow* Application::showMainWindow(bool minimized)
m_mainWindow = new MainWindow(); m_mainWindow = new MainWindow();
m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toByteArray())); m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toByteArray()));
m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray())); m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray()));
#ifdef Q_OS_WIN
if (QString::compare(settings()->get("ApplicationTheme").toString(), "dark") == 0) {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true);
} else {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false);
}
#endif
if(minimized) if(minimized)
{ {
m_mainWindow->showMinimized(); m_mainWindow->showMinimized();
@ -1372,10 +1321,6 @@ MainWindow* Application::showMainWindow(bool minimized)
} }
m_mainWindow->checkInstancePathForProblems(); m_mainWindow->checkInstancePathForProblems();
<<<<<<< HEAD
=======
connect(this, &Application::updateAllowedChanged, m_mainWindow, &MainWindow::updatesAllowedChanged);
>>>>>>> upstream/develop
connect(m_mainWindow, &MainWindow::isClosing, this, &Application::on_windowClose); connect(m_mainWindow, &MainWindow::isClosing, this, &Application::on_windowClose);
m_openWindows++; m_openWindows++;
} }
@ -1517,14 +1462,30 @@ shared_qobject_ptr<Meta::Index> Application::metadataIndex()
return m_metadataIndex; return m_metadataIndex;
} }
Application::Capabilities Application::currentCapabilities() void Application::updateCapabilities()
{ {
Capabilities c; m_capabilities = None;
if (!getMSAClientID().isEmpty()) if (!getMSAClientID().isEmpty())
c |= SupportsMSA; m_capabilities |= SupportsMSA;
if (!getFlameAPIKey().isEmpty()) if (!getFlameAPIKey().isEmpty())
c |= SupportsFlame; m_capabilities |= SupportsFlame;
return c;
#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) QString Application::getJarPath(QString jarFile)

View File

@ -93,6 +93,8 @@ public:
SupportsMSA = 1 << 0, SupportsMSA = 1 << 0,
SupportsFlame = 1 << 1, SupportsFlame = 1 << 1,
SupportsGameMode = 1 << 2,
SupportsMangoHud = 1 << 3,
}; };
Q_DECLARE_FLAGS(Capabilities, Capability) Q_DECLARE_FLAGS(Capabilities, Capability)
@ -156,7 +158,7 @@ public:
shared_qobject_ptr<Meta::Index> metadataIndex(); shared_qobject_ptr<Meta::Index> metadataIndex();
Capabilities currentCapabilities(); void updateCapabilities();
/*! /*!
* Finds and returns the full path to a jar file. * Finds and returns the full path to a jar file.
@ -174,6 +176,10 @@ public:
return m_rootPath; 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 * Opens a json file using either a system default editor, or, if not empty, the editor
* specified in the settings * specified in the settings
@ -197,6 +203,7 @@ public slots:
bool launch( bool launch(
InstancePtr instance, InstancePtr instance,
bool online = true, bool online = true,
bool demo = false,
BaseProfilerFactory *profiler = nullptr, BaseProfilerFactory *profiler = nullptr,
MinecraftServerTargetPtr serverToJoin = nullptr, MinecraftServerTargetPtr serverToJoin = nullptr,
MinecraftAccountPtr accountToUse = nullptr MinecraftAccountPtr accountToUse = nullptr
@ -247,6 +254,7 @@ private:
QString m_rootPath; QString m_rootPath;
Status m_status = Application::StartingUp; Status m_status = Application::StartingUp;
Capabilities m_capabilities;
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive; Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive;

View File

@ -53,15 +53,22 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s
: QObject() : QObject()
{ {
m_settings = settings; m_settings = settings;
m_global_settings = globalSettings;
m_rootDir = rootDir; m_rootDir = rootDir;
m_settings->registerSetting("name", "Unnamed Instance"); m_settings->registerSetting("name", "Unnamed Instance");
m_settings->registerSetting("iconKey", "default"); m_settings->registerSetting("iconKey", "default");
m_settings->registerSetting("notes", ""); m_settings->registerSetting("notes", "");
m_settings->registerSetting("lastLaunchTime", 0); m_settings->registerSetting("lastLaunchTime", 0);
m_settings->registerSetting("totalTimePlayed", 0); m_settings->registerSetting("totalTimePlayed", 0);
m_settings->registerSetting("lastTimePlayed", 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 // NOTE: Sometimees InstanceType is already registered, as it was used to identify the type of
// a locally stored instance // a locally stored instance
if (!m_settings->getSetting("InstanceType")) if (!m_settings->getSetting("InstanceType"))
@ -107,49 +114,59 @@ QString BaseInstance::getPostExitCommand()
return settings()->get("PostExitCommand").toString(); return settings()->get("PostExitCommand").toString();
} }
bool BaseInstance::isManagedPack() bool BaseInstance::isManagedPack() const
{ {
return settings()->get("ManagedPack").toBool(); return m_settings->get("ManagedPack").toBool();
} }
QString BaseInstance::getManagedPackType() QString BaseInstance::getManagedPackType() const
{ {
return settings()->get("ManagedPackType").toString(); return m_settings->get("ManagedPackType").toString();
} }
QString BaseInstance::getManagedPackID() QString BaseInstance::getManagedPackID() const
{ {
return settings()->get("ManagedPackID").toString(); return m_settings->get("ManagedPackID").toString();
} }
QString BaseInstance::getManagedPackName() QString BaseInstance::getManagedPackName() const
{ {
return settings()->get("ManagedPackName").toString(); return m_settings->get("ManagedPackName").toString();
} }
QString BaseInstance::getManagedPackVersionID() QString BaseInstance::getManagedPackVersionID() const
{ {
return settings()->get("ManagedPackVersionID").toString(); return m_settings->get("ManagedPackVersionID").toString();
} }
QString BaseInstance::getManagedPackVersionName() QString BaseInstance::getManagedPackVersionName() const
{ {
return settings()->get("ManagedPackVersionName").toString(); return m_settings->get("ManagedPackVersionName").toString();
} }
void BaseInstance::setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version) void BaseInstance::setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version)
{ {
settings()->set("ManagedPack", true); m_settings->set("ManagedPack", true);
settings()->set("ManagedPackType", type); m_settings->set("ManagedPackType", type);
settings()->set("ManagedPackID", id); m_settings->set("ManagedPackID", id);
settings()->set("ManagedPackName", name); m_settings->set("ManagedPackName", name);
settings()->set("ManagedPackVersionID", versionId); m_settings->set("ManagedPackVersionID", versionId);
settings()->set("ManagedPackVersionName", version); m_settings->set("ManagedPackVersionName", version);
}
void BaseInstance::copyManagedPack(BaseInstance& other)
{
m_settings->set("ManagedPack", other.isManagedPack());
m_settings->set("ManagedPackType", other.getManagedPackType());
m_settings->set("ManagedPackID", other.getManagedPackID());
m_settings->set("ManagedPackName", other.getManagedPackName());
m_settings->set("ManagedPackVersionID", other.getManagedPackVersionID());
m_settings->set("ManagedPackVersionName", other.getManagedPackVersionName());
} }
int BaseInstance::getConsoleMaxLines() const int BaseInstance::getConsoleMaxLines() const
{ {
auto lineSetting = settings()->getSetting("ConsoleMaxLines"); auto lineSetting = m_settings->getSetting("ConsoleMaxLines");
bool conversionOk = false; bool conversionOk = false;
int maxLines = lineSetting->get().toInt(&conversionOk); int maxLines = lineSetting->get().toInt(&conversionOk);
if(!conversionOk) if(!conversionOk)
@ -162,7 +179,7 @@ int BaseInstance::getConsoleMaxLines() const
bool BaseInstance::shouldStopOnConsoleOverflow() const bool BaseInstance::shouldStopOnConsoleOverflow() const
{ {
return settings()->get("ConsoleOverflowStop").toBool(); return m_settings->get("ConsoleOverflowStop").toBool();
} }
void BaseInstance::iconUpdated(QString key) void BaseInstance::iconUpdated(QString key)
@ -237,7 +254,7 @@ void BaseInstance::setRunning(bool running)
int64_t BaseInstance::totalTimePlayed() const int64_t BaseInstance::totalTimePlayed() const
{ {
qint64 current = settings()->get("totalTimePlayed").toLongLong(); qint64 current = m_settings->get("totalTimePlayed").toLongLong();
if(m_isRunning) if(m_isRunning)
{ {
QDateTime timeNow = QDateTime::currentDateTime(); QDateTime timeNow = QDateTime::currentDateTime();
@ -253,7 +270,7 @@ int64_t BaseInstance::lastTimePlayed() const
QDateTime timeNow = QDateTime::currentDateTime(); QDateTime timeNow = QDateTime::currentDateTime();
return m_timeStarted.secsTo(timeNow); return m_timeStarted.secsTo(timeNow);
} }
return settings()->get("lastTimePlayed").toLongLong(); return m_settings->get("lastTimePlayed").toLongLong();
} }
void BaseInstance::resetTimePlayed() void BaseInstance::resetTimePlayed()
@ -272,8 +289,10 @@ QString BaseInstance::instanceRoot() const
return m_rootDir; return m_rootDir;
} }
SettingsObjectPtr BaseInstance::settings() const SettingsObjectPtr BaseInstance::settings()
{ {
loadSpecificSettings();
return m_settings; return m_settings;
} }
@ -340,7 +359,7 @@ QString BaseInstance::windowTitle() const
} }
// FIXME: why is this here? move it to MinecraftInstance!!! // FIXME: why is this here? move it to MinecraftInstance!!!
QStringList BaseInstance::extraArguments() const QStringList BaseInstance::extraArguments()
{ {
return Commandline::splitArgs(settings()->get("JvmArgs").toString()); return Commandline::splitArgs(settings()->get("JvmArgs").toString());
} }
@ -349,3 +368,8 @@ shared_qobject_ptr<LaunchTask> BaseInstance::getLaunchTask()
{ {
return m_launchProcess; return m_launchProcess;
} }
void BaseInstance::updateRuntimeContext()
{
// NOOP
}

View File

@ -54,6 +54,7 @@
#include "net/Mode.h" #include "net/Mode.h"
#include "minecraft/launch/MinecraftServerTarget.h" #include "minecraft/launch/MinecraftServerTarget.h"
#include "RuntimeContext.h"
class QDir; class QDir;
class Task; class Task;
@ -140,13 +141,14 @@ public:
QString getPostExitCommand(); QString getPostExitCommand();
QString getWrapperCommand(); QString getWrapperCommand();
bool isManagedPack(); bool isManagedPack() const;
QString getManagedPackType(); QString getManagedPackType() const;
QString getManagedPackID(); QString getManagedPackID() const;
QString getManagedPackName(); QString getManagedPackName() const;
QString getManagedPackVersionID(); QString getManagedPackVersionID() const;
QString getManagedPackVersionName(); QString getManagedPackVersionName() const;
void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version); void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version);
void copyManagedPack(BaseInstance& other);
/// guess log level from a line of game log /// guess log level from a line of game log
virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level)
@ -154,7 +156,7 @@ public:
return level; return level;
}; };
virtual QStringList extraArguments() const; virtual QStringList extraArguments();
/// Traits. Normally inside the version, depends on instance implementation. /// Traits. Normally inside the version, depends on instance implementation.
virtual QSet <QString> traits() const = 0; virtual QSet <QString> traits() const = 0;
@ -170,9 +172,18 @@ public:
/*! /*!
* \brief Gets this instance's settings object. * \brief Gets this instance's settings object.
* This settings object stores instance-specific settings. * 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. * \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 /// returns a valid update task
virtual Task::Ptr createUpdateTask(Net::Mode mode) = 0; virtual Task::Ptr createUpdateTask(Net::Mode mode) = 0;
@ -206,10 +217,16 @@ public:
virtual QString instanceConfigFolder() const = 0; virtual QString instanceConfigFolder() const = 0;
/// get variables this instance exports /// get variables this instance exports
virtual QMap<QString, QString> getVariables() const = 0; virtual QMap<QString, QString> getVariables() = 0;
virtual QString typeName() const = 0; virtual QString typeName() const = 0;
void updateRuntimeContext();
RuntimeContext runtimeContext() const
{
return m_runtimeContext;
}
bool hasVersionBroken() const bool hasVersionBroken() const
{ {
return m_hasBrokenVersion; return m_hasBrokenVersion;
@ -268,6 +285,11 @@ public:
protected: protected:
void changeStatus(Status newStatus); 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: signals:
/*! /*!
* \brief Signal emitted when properties relevant to the instance view change * \brief Signal emitted when properties relevant to the instance view change
@ -290,12 +312,17 @@ protected: /* data */
bool m_isRunning = false; bool m_isRunning = false;
shared_qobject_ptr<LaunchTask> m_launchProcess; shared_qobject_ptr<LaunchTask> m_launchProcess;
QDateTime m_timeStarted; QDateTime m_timeStarted;
RuntimeContext m_runtimeContext;
private: /* data */ private: /* data */
Status m_status = Status::Present; Status m_status = Status::Present;
bool m_crashed = false; bool m_crashed = false;
bool m_hasUpdate = false; bool m_hasUpdate = false;
bool m_hasBrokenVersion = false; bool m_hasBrokenVersion = false;
SettingsObjectWeakPtr m_global_settings;
bool m_specific_settings_loaded = false;
}; };
Q_DECLARE_METATYPE(shared_qobject_ptr<BaseInstance>) Q_DECLARE_METATYPE(shared_qobject_ptr<BaseInstance>)

View File

@ -26,6 +26,7 @@ set(CORE_SOURCES
MMCZip.cpp MMCZip.cpp
MMCStrings.h MMCStrings.h
MMCStrings.cpp MMCStrings.cpp
RuntimeContext.h
# Basic instance manipulation tasks (derived from InstanceTask) # Basic instance manipulation tasks (derived from InstanceTask)
InstanceCreationTask.h InstanceCreationTask.h
@ -88,12 +89,6 @@ set(CORE_SOURCES
MMCTime.cpp 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 set(PATHMATCHER_SOURCES
# Path matchers # Path matchers
pathmatcher/FSTreeMatcher.h pathmatcher/FSTreeMatcher.h
@ -278,8 +273,6 @@ set(MINECRAFT_SOURCES
minecraft/Rule.h minecraft/Rule.h
minecraft/OneSixVersionFormat.cpp minecraft/OneSixVersionFormat.cpp
minecraft/OneSixVersionFormat.h minecraft/OneSixVersionFormat.h
minecraft/OpSys.cpp
minecraft/OpSys.h
minecraft/ParseUtils.cpp minecraft/ParseUtils.cpp
minecraft/ParseUtils.h minecraft/ParseUtils.h
minecraft/ProfileUtils.cpp minecraft/ProfileUtils.cpp
@ -287,6 +280,8 @@ set(MINECRAFT_SOURCES
minecraft/Library.cpp minecraft/Library.cpp
minecraft/Library.h minecraft/Library.h
minecraft/MojangDownloadInfo.h minecraft/MojangDownloadInfo.h
minecraft/VanillaInstanceCreationTask.cpp
minecraft/VanillaInstanceCreationTask.h
minecraft/VersionFile.cpp minecraft/VersionFile.cpp
minecraft/VersionFile.h minecraft/VersionFile.h
minecraft/VersionFilterData.h minecraft/VersionFilterData.h
@ -302,16 +297,30 @@ set(MINECRAFT_SOURCES
minecraft/mod/ModDetails.h minecraft/mod/ModDetails.h
minecraft/mod/ModFolderModel.h minecraft/mod/ModFolderModel.h
minecraft/mod/ModFolderModel.cpp 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.h
minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/ResourcePackFolderModel.cpp
minecraft/mod/TexturePack.h
minecraft/mod/TexturePack.cpp
minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.h
minecraft/mod/TexturePackFolderModel.cpp minecraft/mod/TexturePackFolderModel.cpp
minecraft/mod/ShaderPackFolderModel.h
minecraft/mod/tasks/BasicFolderLoadTask.h
minecraft/mod/tasks/ModFolderLoadTask.h minecraft/mod/tasks/ModFolderLoadTask.h
minecraft/mod/tasks/ModFolderLoadTask.cpp minecraft/mod/tasks/ModFolderLoadTask.cpp
minecraft/mod/tasks/LocalModParseTask.h minecraft/mod/tasks/LocalModParseTask.h
minecraft/mod/tasks/LocalModParseTask.cpp minecraft/mod/tasks/LocalModParseTask.cpp
minecraft/mod/tasks/LocalModUpdateTask.h minecraft/mod/tasks/LocalModUpdateTask.h
minecraft/mod/tasks/LocalModUpdateTask.cpp minecraft/mod/tasks/LocalModUpdateTask.cpp
minecraft/mod/tasks/LocalResourcePackParseTask.h
minecraft/mod/tasks/LocalResourcePackParseTask.cpp
minecraft/mod/tasks/LocalTexturePackParseTask.h
minecraft/mod/tasks/LocalTexturePackParseTask.cpp
# Assets # Assets
minecraft/AssetsUtils.h minecraft/AssetsUtils.h
@ -329,42 +338,6 @@ set(MINECRAFT_SOURCES
mojang/PackageManifest.cpp mojang/PackageManifest.cpp
minecraft/Agent.h) 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 # the screenshots feature
set(SCREENSHOTS_SOURCES set(SCREENSHOTS_SOURCES
screenshots/Screenshot.h screenshots/Screenshot.h
@ -386,9 +359,6 @@ set(TASKS_SOURCES
tasks/MultipleOptionsTask.cpp 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 set(SETTINGS_SOURCES
# Settings # Settings
settings/INIFile.cpp settings/INIFile.cpp
@ -405,9 +375,6 @@ set(SETTINGS_SOURCES
settings/SettingsObject.h 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 set(JAVA_SOURCES
java/JavaChecker.h java/JavaChecker.h
java/JavaChecker.cpp java/JavaChecker.cpp
@ -423,9 +390,6 @@ set(JAVA_SOURCES
java/JavaVersion.cpp 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 set(TRANSLATIONS_SOURCES
translations/TranslationsModel.h translations/TranslationsModel.h
translations/TranslationsModel.cpp translations/TranslationsModel.cpp
@ -478,6 +442,10 @@ set(API_SOURCES
modplatform/modrinth/ModrinthAPI.cpp modplatform/modrinth/ModrinthAPI.cpp
modplatform/helpers/NetworkModAPI.h modplatform/helpers/NetworkModAPI.h
modplatform/helpers/NetworkModAPI.cpp modplatform/helpers/NetworkModAPI.cpp
modplatform/helpers/HashUtils.h
modplatform/helpers/HashUtils.cpp
modplatform/helpers/OverrideUtils.h
modplatform/helpers/OverrideUtils.cpp
) )
set(FTB_SOURCES set(FTB_SOURCES
@ -503,6 +471,8 @@ set(FLAME_SOURCES
modplatform/flame/FileResolvingTask.cpp modplatform/flame/FileResolvingTask.cpp
modplatform/flame/FlameCheckUpdate.cpp modplatform/flame/FlameCheckUpdate.cpp
modplatform/flame/FlameCheckUpdate.h modplatform/flame/FlameCheckUpdate.h
modplatform/flame/FlameInstanceCreationTask.h
modplatform/flame/FlameInstanceCreationTask.cpp
) )
set(MODRINTH_SOURCES set(MODRINTH_SOURCES
@ -512,6 +482,8 @@ set(MODRINTH_SOURCES
modplatform/modrinth/ModrinthPackManifest.h modplatform/modrinth/ModrinthPackManifest.h
modplatform/modrinth/ModrinthCheckUpdate.cpp modplatform/modrinth/ModrinthCheckUpdate.cpp
modplatform/modrinth/ModrinthCheckUpdate.h modplatform/modrinth/ModrinthCheckUpdate.h
modplatform/modrinth/ModrinthInstanceCreationTask.cpp
modplatform/modrinth/ModrinthInstanceCreationTask.h
) )
set(MODPACKSCH_SOURCES set(MODPACKSCH_SOURCES
@ -526,9 +498,6 @@ set(PACKWIZ_SOURCES
modplatform/packwiz/Packwiz.cpp 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 set(TECHNIC_SOURCES
modplatform/technic/SingleZipPackInstallTask.h modplatform/technic/SingleZipPackInstallTask.h
@ -552,9 +521,6 @@ set(ATLAUNCHER_SOURCES
modplatform/atlauncher/ATLShareCode.h modplatform/atlauncher/ATLShareCode.h
) )
ecm_add_test(meta/Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME Index)
################################ COMPILE ################################ ################################ COMPILE ################################
# we need zlib # we need zlib
@ -745,6 +711,8 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h
ui/pages/modplatform/atlauncher/AtlPage.cpp ui/pages/modplatform/atlauncher/AtlPage.cpp
ui/pages/modplatform/atlauncher/AtlPage.h 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.cpp
ui/pages/modplatform/ftb/FtbFilterModel.h ui/pages/modplatform/ftb/FtbFilterModel.h
@ -828,6 +796,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ModDownloadDialog.h ui/dialogs/ModDownloadDialog.h
ui/dialogs/ScrollMessageBox.cpp ui/dialogs/ScrollMessageBox.cpp
ui/dialogs/ScrollMessageBox.h ui/dialogs/ScrollMessageBox.h
ui/dialogs/BlockedModsDialog.cpp
ui/dialogs/BlockedModsDialog.h
ui/dialogs/ChooseProviderDialog.h ui/dialogs/ChooseProviderDialog.h
ui/dialogs/ChooseProviderDialog.cpp ui/dialogs/ChooseProviderDialog.cpp
ui/dialogs/ModUpdateDialog.cpp ui/dialogs/ModUpdateDialog.cpp
@ -854,8 +824,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/LineSeparator.h ui/widgets/LineSeparator.h
ui/widgets/LogView.cpp ui/widgets/LogView.cpp
ui/widgets/LogView.h ui/widgets/LogView.h
ui/widgets/MCModInfoFrame.cpp ui/widgets/InfoFrame.cpp
ui/widgets/MCModInfoFrame.h ui/widgets/InfoFrame.h
ui/widgets/ModFilterWidget.cpp ui/widgets/ModFilterWidget.cpp
ui/widgets/ModFilterWidget.h ui/widgets/ModFilterWidget.h
ui/widgets/ModListView.cpp ui/widgets/ModListView.cpp
@ -863,6 +833,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/PageContainer.cpp ui/widgets/PageContainer.cpp
ui/widgets/PageContainer.h ui/widgets/PageContainer.h
ui/widgets/PageContainer_p.h ui/widgets/PageContainer_p.h
ui/widgets/ProjectItem.h
ui/widgets/ProjectItem.cpp
ui/widgets/VersionListView.cpp ui/widgets/VersionListView.cpp
ui/widgets/VersionListView.h ui/widgets/VersionListView.h
ui/widgets/VersionSelectWidget.cpp ui/widgets/VersionSelectWidget.cpp
@ -886,6 +858,16 @@ SET(LAUNCHER_SOURCES
ui/instanceview/VisualGroup.h ui/instanceview/VisualGroup.h
) )
if(WIN32)
set(LAUNCHER_SOURCES
${LAUNCHER_SOURCES}
# GUI - dark titlebar for Windows 10/11
ui/WinDarkmode.h
ui/WinDarkmode.cpp
)
endif()
qt_wrap_ui(LAUNCHER_UI qt_wrap_ui(LAUNCHER_UI
ui/setupwizard/PasteWizardPage.ui ui/setupwizard/PasteWizardPage.ui
ui/pages/global/AccountListPage.ui ui/pages/global/AccountListPage.ui
@ -917,7 +899,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/pages/modplatform/technic/TechnicPage.ui ui/pages/modplatform/technic/TechnicPage.ui
ui/widgets/InstanceCardWidget.ui ui/widgets/InstanceCardWidget.ui
ui/widgets/CustomCommands.ui ui/widgets/CustomCommands.ui
ui/widgets/MCModInfoFrame.ui ui/widgets/InfoFrame.ui
ui/widgets/ModFilterWidget.ui ui/widgets/ModFilterWidget.ui
ui/dialogs/CopyInstanceDialog.ui ui/dialogs/CopyInstanceDialog.ui
ui/dialogs/ProfileSetupDialog.ui ui/dialogs/ProfileSetupDialog.ui
@ -936,6 +918,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/EditAccountDialog.ui ui/dialogs/EditAccountDialog.ui
ui/dialogs/ReviewMessageBox.ui ui/dialogs/ReviewMessageBox.ui
ui/dialogs/ScrollMessageBox.ui ui/dialogs/ScrollMessageBox.ui
ui/dialogs/BlockedModsDialog.ui
ui/dialogs/ChooseProviderDialog.ui ui/dialogs/ChooseProviderDialog.ui
) )
@ -960,17 +943,17 @@ endif()
# Add executable # Add executable
add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) 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 target_link_libraries(Launcher_logic
systeminfo systeminfo
Launcher_classparser
Launcher_murmur2 Launcher_murmur2
nbt++ nbt++
${ZLIB_LIBRARIES} ${ZLIB_LIBRARIES}
optional-bare tomlplusplus::tomlplusplus
tomlc99
BuildConfig BuildConfig
Katabasis Katabasis
Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Widgets
ghcFilesystem::ghc_filesystem
) )
if (UNIX AND NOT CYGWIN AND NOT APPLE) if (UNIX AND NOT CYGWIN AND NOT APPLE)

View File

@ -92,412 +92,4 @@ QStringList splitArgs(QString args)
argv << current; argv << current;
return argv; return argv;
} }
Parser::Parser(FlagStyle::Enum flagStyle, ArgumentStyle::Enum argStyle)
{
m_flagStyle = flagStyle;
m_argStyle = argStyle;
}
// styles setter/getter
void Parser::setArgumentStyle(ArgumentStyle::Enum style)
{
m_argStyle = style;
}
ArgumentStyle::Enum Parser::argumentStyle()
{
return m_argStyle;
}
void Parser::setFlagStyle(FlagStyle::Enum style)
{
m_flagStyle = style;
}
FlagStyle::Enum Parser::flagStyle()
{
return m_flagStyle;
}
// setup methods
void Parser::addSwitch(QString name, bool def)
{
if (m_params.contains(name))
throw "Name not unique";
OptionDef *param = new OptionDef;
param->type = otSwitch;
param->name = name;
param->metavar = QString("<%1>").arg(name);
param->def = def;
m_options[name] = param;
m_params[name] = (CommonDef *)param;
m_optionList.append(param);
}
void Parser::addOption(QString name, QVariant def)
{
if (m_params.contains(name))
throw "Name not unique";
OptionDef *param = new OptionDef;
param->type = otOption;
param->name = name;
param->metavar = QString("<%1>").arg(name);
param->def = def;
m_options[name] = param;
m_params[name] = (CommonDef *)param;
m_optionList.append(param);
}
void Parser::addArgument(QString name, bool required, QVariant def)
{
if (m_params.contains(name))
throw "Name not unique";
PositionalDef *param = new PositionalDef;
param->name = name;
param->def = def;
param->required = required;
param->metavar = name;
m_positionals.append(param);
m_params[name] = (CommonDef *)param;
}
void Parser::addDocumentation(QString name, QString doc, QString metavar)
{
if (!m_params.contains(name))
throw "Name does not exist";
CommonDef *param = m_params[name];
param->doc = doc;
if (!metavar.isNull())
param->metavar = metavar;
}
void Parser::addShortOpt(QString name, QChar flag)
{
if (!m_params.contains(name))
throw "Name does not exist";
if (!m_options.contains(name))
throw "Name is not an Option or Swtich";
OptionDef *param = m_options[name];
m_flags[flag] = param;
param->flag = flag;
}
// help methods
QString Parser::compileHelp(QString progName, int helpIndent, bool useFlags)
{
QStringList help;
help << compileUsage(progName, useFlags) << "\r\n";
// positionals
if (!m_positionals.isEmpty())
{
help << "\r\n";
help << "Positional arguments:\r\n";
QListIterator<PositionalDef *> it2(m_positionals);
while (it2.hasNext())
{
PositionalDef *param = it2.next();
help << " " << param->metavar;
help << " " << QString(helpIndent - param->metavar.length() - 1, ' ');
help << param->doc << "\r\n";
}
}
// Options
if (!m_optionList.isEmpty())
{
help << "\r\n";
QString optPrefix, flagPrefix;
getPrefix(optPrefix, flagPrefix);
help << "Options & Switches:\r\n";
QListIterator<OptionDef *> it(m_optionList);
while (it.hasNext())
{
OptionDef *option = it.next();
help << " ";
int nameLength = optPrefix.length() + option->name.length();
if (!option->flag.isNull())
{
nameLength += 3 + flagPrefix.length();
help << flagPrefix << option->flag << ", ";
}
help << optPrefix << option->name;
if (option->type == otOption)
{
QString arg = QString("%1%2").arg(
((m_argStyle == ArgumentStyle::Equals) ? "=" : " "), option->metavar);
nameLength += arg.length();
help << arg;
}
help << " " << QString(helpIndent - nameLength - 1, ' ');
help << option->doc << "\r\n";
}
}
return help.join("");
}
QString Parser::compileUsage(QString progName, bool useFlags)
{
QStringList usage;
usage << "Usage: " << progName;
QString optPrefix, flagPrefix;
getPrefix(optPrefix, flagPrefix);
// options
QListIterator<OptionDef *> it(m_optionList);
while (it.hasNext())
{
OptionDef *option = it.next();
usage << " [";
if (!option->flag.isNull() && useFlags)
usage << flagPrefix << option->flag;
else
usage << optPrefix << option->name;
if (option->type == otOption)
usage << ((m_argStyle == ArgumentStyle::Equals) ? "=" : " ") << option->metavar;
usage << "]";
}
// arguments
QListIterator<PositionalDef *> it2(m_positionals);
while (it2.hasNext())
{
PositionalDef *param = it2.next();
usage << " " << (param->required ? "<" : "[");
usage << param->metavar;
usage << (param->required ? ">" : "]");
}
return usage.join("");
}
// parsing
QHash<QString, QVariant> Parser::parse(QStringList argv)
{
QHash<QString, QVariant> map;
QStringListIterator it(argv);
QString programName = it.next();
QString optionPrefix;
QString flagPrefix;
QListIterator<PositionalDef *> positionals(m_positionals);
QStringList expecting;
getPrefix(optionPrefix, flagPrefix);
while (it.hasNext())
{
QString arg = it.next();
if (!expecting.isEmpty())
// we were expecting an argument
{
QString name = expecting.first();
/*
if (map.contains(name))
throw ParsingError(
QString("Option %2%1 was given multiple times").arg(name, optionPrefix));
*/
map[name] = QVariant(arg);
expecting.removeFirst();
continue;
}
if (arg.startsWith(optionPrefix))
// we have an option
{
// qDebug("Found option %s", qPrintable(arg));
QString name = arg.mid(optionPrefix.length());
QString equals;
if ((m_argStyle == ArgumentStyle::Equals ||
m_argStyle == ArgumentStyle::SpaceAndEquals) &&
name.contains("="))
{
int i = name.indexOf("=");
equals = name.mid(i + 1);
name = name.left(i);
}
if (m_options.contains(name))
{
/*
if (map.contains(name))
throw ParsingError(QString("Option %2%1 was given multiple times")
.arg(name, optionPrefix));
*/
OptionDef *option = m_options[name];
if (option->type == otSwitch)
map[name] = true;
else // if (option->type == otOption)
{
if (m_argStyle == ArgumentStyle::Space)
expecting.append(name);
else if (!equals.isNull())
map[name] = equals;
else if (m_argStyle == ArgumentStyle::SpaceAndEquals)
expecting.append(name);
else
throw ParsingError(QString("Option %2%1 reqires an argument.")
.arg(name, optionPrefix));
}
continue;
}
throw ParsingError(QString("Unknown Option %2%1").arg(name, optionPrefix));
}
if (arg.startsWith(flagPrefix))
// we have (a) flag(s)
{
// qDebug("Found flags %s", qPrintable(arg));
QString flags = arg.mid(flagPrefix.length());
QString equals;
if ((m_argStyle == ArgumentStyle::Equals ||
m_argStyle == ArgumentStyle::SpaceAndEquals) &&
flags.contains("="))
{
int i = flags.indexOf("=");
equals = flags.mid(i + 1);
flags = flags.left(i);
}
for (int i = 0; i < flags.length(); i++)
{
QChar flag = flags.at(i);
if (!m_flags.contains(flag))
throw ParsingError(QString("Unknown flag %2%1").arg(flag, flagPrefix));
OptionDef *option = m_flags[flag];
/*
if (map.contains(option->name))
throw ParsingError(QString("Option %2%1 was given multiple times")
.arg(option->name, optionPrefix));
*/
if (option->type == otSwitch)
map[option->name] = true;
else // if (option->type == otOption)
{
if (m_argStyle == ArgumentStyle::Space)
expecting.append(option->name);
else if (!equals.isNull())
if (i == flags.length() - 1)
map[option->name] = equals;
else
throw ParsingError(QString("Flag %4%2 of Argument-requiring Option "
"%1 not last flag in %4%3")
.arg(option->name, flag, flags, flagPrefix));
else if (m_argStyle == ArgumentStyle::SpaceAndEquals)
expecting.append(option->name);
else
throw ParsingError(QString("Option %1 reqires an argument. (flag %3%2)")
.arg(option->name, flag, flagPrefix));
}
}
continue;
}
// must be a positional argument
if (!positionals.hasNext())
throw ParsingError(QString("Don't know what to do with '%1'").arg(arg));
PositionalDef *param = positionals.next();
map[param->name] = arg;
}
// check if we're missing something
if (!expecting.isEmpty())
throw ParsingError(QString("Was still expecting arguments for %2%1").arg(
expecting.join(QString(", ") + optionPrefix), optionPrefix));
while (positionals.hasNext())
{
PositionalDef *param = positionals.next();
if (param->required)
throw ParsingError(
QString("Missing required positional argument '%1'").arg(param->name));
else
map[param->name] = param->def;
}
// fill out gaps
QListIterator<OptionDef *> iter(m_optionList);
while (iter.hasNext())
{
OptionDef *option = iter.next();
if (!map.contains(option->name))
map[option->name] = option->def;
}
return map;
}
// clear defs
void Parser::clear()
{
m_flags.clear();
m_params.clear();
m_options.clear();
QMutableListIterator<OptionDef *> it(m_optionList);
while (it.hasNext())
{
OptionDef *option = it.next();
it.remove();
delete option;
}
QMutableListIterator<PositionalDef *> it2(m_positionals);
while (it2.hasNext())
{
PositionalDef *arg = it2.next();
it2.remove();
delete arg;
}
}
// Destructor
Parser::~Parser()
{
clear();
}
// getPrefix
void Parser::getPrefix(QString &opt, QString &flag)
{
if (m_flagStyle == FlagStyle::Windows)
opt = flag = "/";
else if (m_flagStyle == FlagStyle::Unix)
opt = flag = "-";
// else if (m_flagStyle == FlagStyle::GNU)
else
{
opt = "--";
flag = "-";
}
}
// ParsingError
ParsingError::ParsingError(const QString &what) : std::runtime_error(what.toStdString())
{
}
} }

View File

@ -17,12 +17,7 @@
#pragma once #pragma once
#include <exception>
#include <stdexcept>
#include <QString> #include <QString>
#include <QVariant>
#include <QHash>
#include <QStringList> #include <QStringList>
/** /**
@ -39,212 +34,4 @@ namespace Commandline
* @return a QStringList containing all arguments * @return a QStringList containing all arguments
*/ */
QStringList splitArgs(QString args); QStringList splitArgs(QString args);
/**
* @brief The FlagStyle enum
* Specifies how flags are decorated
*/
namespace FlagStyle
{
enum Enum
{
GNU, /**< --option and -o (GNU Style) */
Unix, /**< -option and -o (Unix Style) */
Windows, /**< /option and /o (Windows Style) */
#ifdef Q_OS_WIN32
Default = Windows
#else
Default = GNU
#endif
};
}
/**
* @brief The ArgumentStyle enum
*/
namespace ArgumentStyle
{
enum Enum
{
Space, /**< --option value */
Equals, /**< --option=value */
SpaceAndEquals, /**< --option[= ]value */
#ifdef Q_OS_WIN32
Default = Equals
#else
Default = SpaceAndEquals
#endif
};
}
/**
* @brief The ParsingError class
*/
class ParsingError : public std::runtime_error
{
public:
ParsingError(const QString &what);
};
/**
* @brief The Parser class
*/
class Parser
{
public:
/**
* @brief Parser constructor
* @param flagStyle the FlagStyle to use in this Parser
* @param argStyle the ArgumentStyle to use in this Parser
*/
Parser(FlagStyle::Enum flagStyle = FlagStyle::Default,
ArgumentStyle::Enum argStyle = ArgumentStyle::Default);
/**
* @brief set the flag style
* @param style
*/
void setFlagStyle(FlagStyle::Enum style);
/**
* @brief get the flag style
* @return
*/
FlagStyle::Enum flagStyle();
/**
* @brief set the argument style
* @param style
*/
void setArgumentStyle(ArgumentStyle::Enum style);
/**
* @brief get the argument style
* @return
*/
ArgumentStyle::Enum argumentStyle();
/**
* @brief define a boolean switch
* @param name the parameter name
* @param def the default value
*/
void addSwitch(QString name, bool def = false);
/**
* @brief define an option that takes an additional argument
* @param name the parameter name
* @param def the default value
*/
void addOption(QString name, QVariant def = QVariant());
/**
* @brief define a positional argument
* @param name the parameter name
* @param required wether this argument is required
* @param def the default value
*/
void addArgument(QString name, bool required = true, QVariant def = QVariant());
/**
* @brief adds a flag to an existing parameter
* @param name the (existing) parameter name
* @param flag the flag character
* @see addSwitch addArgument addOption
* Note: any one parameter can only have one flag
*/
void addShortOpt(QString name, QChar flag);
/**
* @brief adds documentation to a Parameter
* @param name the parameter name
* @param metavar a string to be displayed as placeholder for the value
* @param doc a QString containing the documentation
* Note: on positional arguments, metavar replaces the name as displayed.
* on options , metavar replaces the value placeholder
*/
void addDocumentation(QString name, QString doc, QString metavar = QString());
/**
* @brief generate a help message
* @param progName the program name to use in the help message
* @param helpIndent how much the parameter documentation should be indented
* @param flagsInUsage whether we should use flags instead of options in the usage
* @return a help message
*/
QString compileHelp(QString progName, int helpIndent = 22, bool flagsInUsage = true);
/**
* @brief generate a short usage message
* @param progName the program name to use in the usage message
* @param useFlags whether we should use flags instead of options
* @return a usage message
*/
QString compileUsage(QString progName, bool useFlags = true);
/**
* @brief parse
* @param argv a QStringList containing the program ARGV
* @return a QHash mapping argument names to their values
*/
QHash<QString, QVariant> parse(QStringList argv);
/**
* @brief clear all definitions
*/
void clear();
~Parser();
private:
FlagStyle::Enum m_flagStyle;
ArgumentStyle::Enum m_argStyle;
enum OptionType
{
otSwitch,
otOption
};
// Important: the common part MUST BE COMMON ON ALL THREE structs
struct CommonDef
{
QString name;
QString doc;
QString metavar;
QVariant def;
};
struct OptionDef
{
// common
QString name;
QString doc;
QString metavar;
QVariant def;
// option
OptionType type;
QChar flag;
};
struct PositionalDef
{
// common
QString name;
QString doc;
QString metavar;
QVariant def;
// positional
bool required;
};
QHash<QString, OptionDef *> m_options;
QHash<QChar, OptionDef *> m_flags;
QHash<QString, CommonDef *> m_params;
QList<PositionalDef *> m_positionals;
QList<OptionDef *> m_optionList;
void getPrefix(QString &opt, QString &flag);
};
} }

View File

@ -35,37 +35,71 @@
#include "FileSystem.h" #include "FileSystem.h"
#include <QDir>
#include <QFile>
#include <QSaveFile>
#include <QFileInfo>
#include <QDebug> #include <QDebug>
#include <QUrl> #include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QSaveFile>
#include <QStandardPaths> #include <QStandardPaths>
#include <QTextStream> #include <QTextStream>
#include <QUrl>
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
#include <windows.h>
#include <string>
#include <sys/utime.h>
#include <winnls.h>
#include <shobjidl.h>
#include <objbase.h> #include <objbase.h>
#include <objidl.h> #include <objidl.h>
#include <shlguid.h> #include <shlguid.h>
#include <shlobj.h> #include <shlobj.h>
#include <shobjidl.h>
#include <sys/utime.h>
#include <windows.h>
#include <winnls.h>
#include <string>
#else #else
#include <utime.h> #include <utime.h>
#endif #endif
// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
#ifdef __APPLE__
#include <Availability.h> // for deployment target to support pre-catalina targets without std::fs
#endif // __APPLE__
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
#if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
#define GHC_USE_STD_FS
#include <filesystem>
namespace fs = std::filesystem;
#endif // MacOS min version check
#endif // Other OSes version check
#ifndef GHC_USE_STD_FS
#include <ghc/filesystem.hpp>
namespace fs = ghc::filesystem;
#endif
#if defined Q_OS_WIN32
std::wstring toStdString(QString s)
{
return s.toStdWString();
}
#else
std::string toStdString(QString s)
{
return s.toStdString();
}
#endif
namespace FS { namespace FS {
void ensureExists(const QDir& dir) void ensureExists(const QDir& dir)
{ {
if (!QDir().mkpath(dir.absolutePath())) if (!QDir().mkpath(dir.absolutePath())) {
{ throw FileSystemException("Unable to create folder " + dir.dirName() + " (" + dir.absolutePath() + ")");
throw FileSystemException("Unable to create folder " + dir.dirName() + " (" +
dir.absolutePath() + ")");
} }
} }
@ -73,38 +107,28 @@ void write(const QString &filename, const QByteArray &data)
{ {
ensureExists(QFileInfo(filename).dir()); ensureExists(QFileInfo(filename).dir());
QSaveFile file(filename); QSaveFile file(filename);
if (!file.open(QSaveFile::WriteOnly)) if (!file.open(QSaveFile::WriteOnly)) {
{ throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString());
throw FileSystemException("Couldn't open " + filename + " for writing: " +
file.errorString());
} }
if (data.size() != file.write(data)) if (data.size() != file.write(data)) {
{ throw FileSystemException("Error writing data to " + filename + ": " + file.errorString());
throw FileSystemException("Error writing data to " + filename + ": " +
file.errorString());
} }
if (!file.commit()) if (!file.commit()) {
{ throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString());
throw FileSystemException("Error while committing data to " + filename + ": " +
file.errorString());
} }
} }
QByteArray read(const QString& filename) QByteArray read(const QString& filename)
{ {
QFile file(filename); QFile file(filename);
if (!file.open(QFile::ReadOnly)) if (!file.open(QFile::ReadOnly)) {
{ throw FileSystemException("Unable to open " + filename + " for reading: " + file.errorString());
throw FileSystemException("Unable to open " + filename + " for reading: " +
file.errorString());
} }
const qint64 size = file.size(); const qint64 size = file.size();
QByteArray data(int(size), 0); QByteArray data(int(size), 0);
const qint64 ret = file.read(data.data(), size); const qint64 ret = file.read(data.data(), size);
if (ret == -1 || ret != size) if (ret == -1 || ret != size) {
{ throw FileSystemException("Error reading data from " + filename + ": " + file.errorString());
throw FileSystemException("Error reading data from " + filename + ": " +
file.errorString());
} }
return data; return data;
} }
@ -140,6 +164,8 @@ bool ensureFolderPathExists(QString foldernamepath)
bool copy::operator()(const QString& offset) bool copy::operator()(const QString& offset)
{ {
using copy_opts = fs::copy_options;
// NOTE always deep copy on windows. the alternatives are too messy. // NOTE always deep copy on windows. the alternatives are too messy.
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
m_followSymlinks = true; m_followSymlinks = true;
@ -148,123 +174,63 @@ bool copy::operator()(const QString &offset)
auto src = PathCombine(m_src.absolutePath(), offset); auto src = PathCombine(m_src.absolutePath(), offset);
auto dst = PathCombine(m_dst.absolutePath(), offset); auto dst = PathCombine(m_dst.absolutePath(), offset);
QFileInfo currentSrc(src); std::error_code err;
if (!currentSrc.exists())
return false;
if(!m_followSymlinks && currentSrc.isSymLink()) fs::copy_options opt = copy_opts::none;
{
qDebug() << "creating symlink" << src << " - " << dst; // The default behavior is to follow symlinks
if (!ensureFilePathExists(dst)) if (!m_followSymlinks)
{ opt |= copy_opts::copy_symlinks;
qWarning() << "Cannot create path!";
return false;
} // We can't use copy_opts::recursive because we need to take into account the
return QFile::link(currentSrc.symLinkTarget(), dst); // blacklisted paths, so we iterate over the source directory, and if there's no blacklist
} // match, we copy the file.
else if(currentSrc.isFile()) QDir src_dir(src);
{ QDirIterator source_it(src, QDir::Filter::Files, QDirIterator::Subdirectories);
qDebug() << "copying file" << src << " - " << dst;
if (!ensureFilePathExists(dst)) while (source_it.hasNext()) {
{ auto src_path = source_it.next();
qWarning() << "Cannot create path!"; auto relative_path = src_dir.relativeFilePath(src_path);
return false;
} if (m_blacklist && m_blacklist->matches(relative_path))
return QFile::copy(src, dst);
}
else if(currentSrc.isDir())
{
qDebug() << "recursing" << offset;
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))
{
auto inner_offset = PathCombine(offset, f);
// ignore and skip stuff that matches the blacklist.
if(m_blacklist && m_blacklist->matches(inner_offset))
{
continue; continue;
}
if(!operator()(inner_offset)) auto dst_path = PathCombine(dst, relative_path);
{ ensureFilePathExists(dst_path);
qWarning() << "Failed to copy" << inner_offset;
return false; fs::copy(toStdString(src_path), toStdString(dst_path), opt, err);
if (err) {
qWarning() << "Failed to copy files:" << QString::fromStdString(err.message());
qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path;
} }
} }
}
else return err.value() == 0;
{
qCritical() << "Copy ERROR: Unknown filesystem object:" << src;
return false;
}
return true;
} }
bool deletePath(QString path) bool deletePath(QString path)
{ {
bool OK = true; std::error_code err;
QFileInfo finfo(path);
if(finfo.isFile()) { fs::remove_all(toStdString(path), err);
return QFile::remove(path);
if (err) {
qWarning() << "Failed to remove files:" << QString::fromStdString(err.message());
} }
QDir dir(path); return err.value() == 0;
}
if (!dir.exists()) bool trash(QString path, QString *pathInTrash = nullptr)
{ {
return OK; #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
} return false;
auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden |
QDir::AllDirs | QDir::Files,
QDir::DirsFirst);
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())
{
OK &= QFile::remove(info.absoluteFilePath());
}
else if (info.isDir())
{
OK &= dir.rmdir(info.absoluteFilePath());
}
}
#else #else
// We do not trust Qt with reparse points, but do trust it with unix symlinks. return QFile::moveToTrash(path, pathInTrash);
if(info.isSymLink())
{
OK &= QFile::remove(info.absoluteFilePath());
}
#endif #endif
else if (info.isDir())
{
OK &= deletePath(info.absoluteFilePath());
} }
else if (info.isFile())
{
OK &= QFile::remove(info.absoluteFilePath());
}
else
{
OK = false;
qCritical() << "Delete ERROR: Unknown filesystem object:" << info.absoluteFilePath();
}
}
OK &= dir.rmdir(dir.absolutePath());
return OK;
}
QString PathCombine(const QString& path1, const QString& path2) QString PathCombine(const QString& path1, const QString& path2)
{ {
@ -292,17 +258,14 @@ QString AbsolutePath(QString path)
QString ResolveExecutable(QString path) QString ResolveExecutable(QString path)
{ {
if (path.isEmpty()) if (path.isEmpty()) {
{
return QString(); return QString();
} }
if(!path.contains('/')) if (!path.contains('/')) {
{
path = QStandardPaths::findExecutable(path); path = QStandardPaths::findExecutable(path);
} }
QFileInfo pathInfo(path); QFileInfo pathInfo(path);
if(!pathInfo.exists() || !pathInfo.isExecutable()) if (!pathInfo.exists() || !pathInfo.isExecutable()) {
{
return QString(); return QString();
} }
return pathInfo.absoluteFilePath(); return pathInfo.absoluteFilePath();
@ -322,12 +285,9 @@ QString NormalizePath(QString path)
QDir b(path); QDir b(path);
QString newAbsolute = b.absolutePath(); QString newAbsolute = b.absolutePath();
if (newAbsolute.startsWith(currentAbsolute)) if (newAbsolute.startsWith(currentAbsolute)) {
{
return a.relativeFilePath(newAbsolute); return a.relativeFilePath(newAbsolute);
} } else {
else
{
return newAbsolute; return newAbsolute;
} }
} }
@ -336,10 +296,8 @@ QString badFilenameChars = "\"\\/?<>:;*|!+\r\n";
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{ {
for (int i = 0; i < string.length(); i++) for (int i = 0; i < string.length(); i++) {
{ if (badFilenameChars.contains(string[i])) {
if (badFilenameChars.contains(string[i]))
{
string[i] = replaceWith; string[i] = replaceWith;
} }
} }
@ -351,15 +309,11 @@ QString DirNameFromString(QString string, QString inDir)
int num = 0; int num = 0;
QString baseName = RemoveInvalidFilenameChars(string, '-'); QString baseName = RemoveInvalidFilenameChars(string, '-');
QString dirName; QString dirName;
do do {
{ if (num == 0) {
if(num == 0)
{
dirName = baseName; dirName = baseName;
} } else {
else dirName = baseName + "(" + QString::number(num) + ")";
{
dirName = baseName + QString::number(num);;
} }
// If it's over 9000 // If it's over 9000
@ -378,63 +332,13 @@ bool checkProblemticPathJava(QDir folder)
return pathfoldername.contains("!", Qt::CaseInsensitive); return pathfoldername.contains("!", Qt::CaseInsensitive);
} }
// Win32 crap
#ifdef Q_OS_WIN
bool called_coinit = false;
HRESULT CreateLink(LPCSTR linkPath, LPCSTR targetPath, LPCSTR args)
{
HRESULT hres;
if (!called_coinit)
{
hres = CoInitialize(NULL);
called_coinit = true;
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);
if (SUCCEEDED(hres))
{
IPersistFile *persistFile;
link->SetPath(targetPath);
link->SetArguments(args);
hres = link->QueryInterface(IID_IPersistFile, (LPVOID *)&persistFile);
if (SUCCEEDED(hres))
{
WCHAR wstr[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, linkPath, -1, wstr, MAX_PATH);
hres = persistFile->Save(wstr, TRUE);
persistFile->Release();
}
link->Release();
}
return hres;
}
#endif
QString getDesktopDir() QString getDesktopDir()
{ {
return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
} }
// Cross-platform Shortcut creation // Cross-platform Shortcut creation
bool createShortCut(QString location, QString dest, QStringList args, QString name, bool createShortCut(QString location, QString dest, QStringList args, QString name, QString icon)
QString icon)
{ {
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
location = PathCombine(location, name + ".desktop"); location = PathCombine(location, name + ".desktop");
@ -459,8 +363,7 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na
stream.flush(); stream.flush();
f.close(); f.close();
f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther);
QFileDevice::ExeOther);
return true; return true;
#elif defined Q_OS_WIN #elif defined Q_OS_WIN
@ -488,46 +391,24 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na
#endif #endif
} }
QStringList listFolderPaths(QDir root)
{
auto createAbsPath = [](QFileInfo const& entry) { return FS::PathCombine(entry.path(), entry.fileName()); };
QStringList entries;
root.refresh();
for (auto entry : root.entryInfoList(QDir::Filter::Files)) {
entries.append(createAbsPath(entry));
}
for (auto entry : root.entryInfoList(QDir::Filter::AllDirs | QDir::Filter::NoDotAndDotDot)) {
entries.append(listFolderPaths(createAbsPath(entry)));
}
return entries;
}
bool overrideFolder(QString overwritten_path, QString override_path) bool overrideFolder(QString overwritten_path, QString override_path)
{ {
using copy_opts = fs::copy_options;
if (!FS::ensureFolderPathExists(overwritten_path)) if (!FS::ensureFolderPathExists(overwritten_path))
return false; return false;
QStringList paths_to_override; std::error_code err;
QDir root_override (override_path); fs::copy_options opt = copy_opts::recursive | copy_opts::overwrite_existing;
for (auto file : listFolderPaths(root_override)) {
QString destination = file;
destination.replace(override_path, overwritten_path);
qDebug() << QString("Applying override %1 in %2").arg(file, destination); fs::copy(toStdString(override_path), toStdString(overwritten_path), opt, err);
if (QFile::exists(destination)) if (err) {
QFile::remove(destination); qCritical() << QString("Failed to apply override from %1 to %2").arg(override_path, overwritten_path);
if (!QFile::rename(file, destination)) { qCritical() << "Reason:" << QString::fromStdString(err.message());
qCritical() << QString("Failed to apply override from %1 to %2").arg(file, destination);
return false;
}
} }
return true; return err.value() == 0;
} }
} }

View File

@ -41,11 +41,9 @@
#include <QDir> #include <QDir>
#include <QFlags> #include <QFlags>
namespace FS namespace FS {
{
class FileSystemException : public ::Exception class FileSystemException : public ::Exception {
{
public: public:
FileSystemException(const QString& message) : Exception(message) {} FileSystemException(const QString& message) : Exception(message) {}
}; };
@ -77,8 +75,7 @@ bool ensureFilePathExists(QString filenamepath);
*/ */
bool ensureFolderPathExists(QString filenamepath); bool ensureFolderPathExists(QString filenamepath);
class copy class copy {
{
public: public:
copy(const QString& src, const QString& dst) copy(const QString& src, const QString& dst)
{ {
@ -95,10 +92,7 @@ public:
m_blacklist = filter; m_blacklist = filter;
return *this; return *this;
} }
bool operator()() bool operator()() { return operator()(QString()); }
{
return operator()(QString());
}
private: private:
bool operator()(const QString& offset); bool operator()(const QString& offset);
@ -115,6 +109,11 @@ private:
*/ */
bool deletePath(QString path); bool deletePath(QString path);
/**
* 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);
QString PathCombine(const QString& path1, const QString& path2, const QString& path3); 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 PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4);

View File

@ -44,7 +44,7 @@ void InstanceCopyTask::copyFinished()
auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg")); auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg"));
InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath)); InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath));
inst->setName(m_instName); inst->setName(name());
inst->setIconKey(m_instIcon); inst->setIconKey(m_instIcon);
if(!m_keepPlaytime) { if(!m_keepPlaytime) {
inst->resetTimePlayed(); inst->resetTimePlayed();

View File

@ -1,40 +1,56 @@
#include "InstanceCreationTask.h" #include "InstanceCreationTask.h"
#include "settings/INISettingsObject.h"
#include "FileSystem.h"
//FIXME: remove this #include <QDebug>
#include "minecraft/MinecraftInstance.h" #include <QFile>
#include "minecraft/PackProfile.h"
InstanceCreationTask::InstanceCreationTask(BaseVersionPtr version) InstanceCreationTask::InstanceCreationTask() = default;
{
m_version = version;
m_usingLoader = false;
}
InstanceCreationTask::InstanceCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loaderVersion)
{
m_version = version;
m_usingLoader = true;
m_loader = loader;
m_loaderVersion = loaderVersion;
}
void InstanceCreationTask::executeTask() void InstanceCreationTask::executeTask()
{ {
setStatus(tr("Creating instance from version %1").arg(m_version->name())); setAbortable(true);
{
auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg")); if (updateInstance()) {
instanceSettings->suspendSave();
MinecraftInstance inst(m_globalSettings, instanceSettings, m_stagingPath);
auto components = inst.getPackProfile();
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", m_version->descriptor(), true);
if(m_usingLoader)
components->setComponentVersion(m_loader, m_loaderVersion->descriptor());
inst.setName(m_instName);
inst.setIconKey(m_instIcon);
instanceSettings->resumeSave();
}
emitSucceeded(); emitSucceeded();
return;
}
// When the user aborted in the update stage.
if (m_abort) {
emitAborted();
return;
}
if (!createInstance()) {
if (m_abort)
return;
qWarning() << "Instance creation failed!";
if (!m_error_message.isEmpty())
qWarning() << "Reason: " << m_error_message;
emitFailed(tr("Error while creating new instance."));
return;
}
// If this is set, it means we're updating an instance. So, we now need to remove the
// files scheduled to, and we'd better not let the user abort in the middle of it, since it'd
// put the instance in an invalid state.
if (shouldOverride()) {
setAbortable(false);
setStatus(tr("Removing old conflicting files..."));
qDebug() << "Removing old files";
for (auto path : m_files_to_remove) {
if (!QFile::exists(path))
continue;
qDebug() << "Removing" << path;
if (!QFile::remove(path)) {
qCritical() << "Couldn't remove the old conflicting files.";
emitFailed(tr("Failed to remove old conflicting files."));
return;
}
}
}
emitSucceeded();
return;
} }

View File

@ -1,26 +1,46 @@
#pragma once #pragma once
#include "tasks/Task.h"
#include "net/NetJob.h"
#include <QUrl>
#include "settings/SettingsObject.h"
#include "BaseVersion.h" #include "BaseVersion.h"
#include "InstanceTask.h" #include "InstanceTask.h"
class InstanceCreationTask : public InstanceTask class InstanceCreationTask : public InstanceTask {
{
Q_OBJECT Q_OBJECT
public: public:
explicit InstanceCreationTask(BaseVersionPtr version); InstanceCreationTask();
explicit InstanceCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loaderVersion); virtual ~InstanceCreationTask() = default;
protected: protected:
//! Entry point for tasks. void executeTask() final override;
virtual void executeTask() override;
private: /* data */ /**
BaseVersionPtr m_version; * Tries to update an already existing instance.
bool m_usingLoader; *
QString m_loader; * This can be implemented by subclasses to provide a way of updating an already existing
BaseVersionPtr m_loaderVersion; * instance, according to that implementation's concept of 'identity' (i.e. instances that
* are updates / downgrades of one another).
*
* If this returns true, createInstance() will not run, so you should do all update steps in here.
* Otherwise, createInstance() is run as normal.
*/
virtual bool updateInstance() { return false; };
/**
* Creates a new instance.
*
* Returns whether the instance creation was successful (true) or not (false).
*/
virtual bool createInstance() { return false; };
QString getError() const { return m_error_message; }
protected:
void setError(QString message) { m_error_message = message; };
protected:
bool m_abort = false;
QStringList m_files_to_remove;
private:
QString m_error_message;
}; };

View File

@ -35,35 +35,26 @@
*/ */
#include "InstanceImportTask.h" #include "InstanceImportTask.h"
#include <QtConcurrentRun>
#include "Application.h" #include "Application.h"
#include "BaseInstance.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "MMCZip.h" #include "MMCZip.h"
#include "NullInstance.h" #include "NullInstance.h"
#include "icons/IconList.h" #include "icons/IconList.h"
#include "icons/IconUtils.h" #include "icons/IconUtils.h"
#include "modplatform/technic/TechnicPackProcessor.h"
#include "modplatform/modrinth/ModrinthInstanceCreationTask.h"
#include "modplatform/flame/FlameInstanceCreationTask.h"
#include "settings/INISettingsObject.h" #include "settings/INISettingsObject.h"
// FIXME: this does not belong here, it's Minecraft/Flame specific #include <QtConcurrentRun>
#include <quazip/quazipdir.h>
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "modplatform/flame/FileResolvingTask.h"
#include "modplatform/flame/PackManifest.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "modplatform/technic/TechnicPackProcessor.h"
#include "Application.h"
#include "icons/IconList.h"
#include "net/ChecksumValidator.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ScrollMessageBox.h"
#include <algorithm> #include <algorithm>
#include <quazip/quazipdir.h>
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent) InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
{ {
m_sourceUrl = sourceUrl; m_sourceUrl = sourceUrl;
@ -72,35 +63,41 @@ InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
bool InstanceImportTask::abort() bool InstanceImportTask::abort()
{ {
if (!canAbort())
return false;
if (m_filesNetJob) if (m_filesNetJob)
m_filesNetJob->abort(); m_filesNetJob->abort();
m_extractFuture.cancel(); m_extractFuture.cancel();
return false; return Task::abort();
} }
void InstanceImportTask::executeTask() void InstanceImportTask::executeTask()
{ {
if (m_sourceUrl.isLocalFile()) setAbortable(true);
{
if (m_sourceUrl.isLocalFile()) {
m_archivePath = m_sourceUrl.toLocalFile(); m_archivePath = m_sourceUrl.toLocalFile();
processZipPack(); processZipPack();
} } else {
else
{
setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
m_downloadRequired = true; m_downloadRequired = true;
const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); const QString path(m_sourceUrl.host() + '/' + m_sourceUrl.path());
auto entry = APPLICATION->metacache()->resolveEntry("general", path); auto entry = APPLICATION->metacache()->resolveEntry("general", path);
entry->setStale(true); entry->setStale(true);
m_archivePath = entry->getFullPath();
m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network()); m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network());
m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
m_archivePath = entry->getFullPath();
auto job = m_filesNetJob.get(); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
connect(job, &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged);
connect(job, &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed);
connect(job, &NetJob::failed, this, &InstanceImportTask::downloadFailed); connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted);
m_filesNetJob->start(); m_filesNetJob->start();
} }
} }
@ -119,7 +116,13 @@ void InstanceImportTask::downloadFailed(QString reason)
void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total) void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total)
{ {
setProgress(current / 2, total); setProgress(current, total);
}
void InstanceImportTask::downloadAborted()
{
emitAborted();
m_filesNetJob.reset();
} }
void InstanceImportTask::processZipPack() void InstanceImportTask::processZipPack()
@ -255,290 +258,31 @@ void InstanceImportTask::extractFinished()
void InstanceImportTask::extractAborted() void InstanceImportTask::extractAborted()
{ {
emitFailed(tr("Instance import has been aborted.")); emitAborted();
return;
} }
void InstanceImportTask::processFlame() void InstanceImportTask::processFlame()
{ {
const static QMap<QString,QString> forgemap = { auto* inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent);
{"1.2.5", "3.4.9.171"},
{"1.4.2", "6.0.1.355"},
{"1.4.7", "6.6.2.534"},
{"1.5.2", "7.8.1.737"}
};
Flame::Manifest pack;
try
{
QString configPath = FS::PathCombine(m_stagingPath, "manifest.json");
Flame::loadManifest(pack, configPath);
QFile::remove(configPath);
}
catch (const JSONValidationError &e)
{
emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
return;
}
if(!pack.overrides.isEmpty())
{
QString overridePath = FS::PathCombine(m_stagingPath, pack.overrides);
if (QFile::exists(overridePath))
{
QString mcPath = FS::PathCombine(m_stagingPath, "minecraft");
if (!QFile::rename(overridePath, mcPath))
{
emitFailed(tr("Could not rename the overrides folder:\n") + pack.overrides);
return;
}
}
else
{
logWarning(tr("The specified overrides folder (%1) is missing. Maybe the modpack was already used before?").arg(pack.overrides));
}
}
QString forgeVersion; inst_creation_task->setName(*this);
QString fabricVersion; inst_creation_task->setIcon(m_instIcon);
// TODO: is Quilt relevant here? inst_creation_task->setGroup(m_instGroup);
for(auto &loader: pack.minecraft.modLoaders)
{
auto id = loader.id;
if(id.startsWith("forge-"))
{
id.remove("forge-");
forgeVersion = id;
continue;
}
if(id.startsWith("fabric-"))
{
id.remove("fabric-");
fabricVersion = id;
continue;
}
logWarning(tr("Unknown mod loader in manifest: %1").arg(id));
}
QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] {
auto instanceSettings = std::make_shared<INISettingsObject>(configPath); setOverride(inst_creation_task->shouldOverride());
MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
auto mcVersion = pack.minecraft.version;
// Hack to correct some 'special sauce'...
if(mcVersion.endsWith('.'))
{
mcVersion.remove(QRegularExpression("[.]+$"));
logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack."));
}
auto components = instance.getPackProfile();
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", mcVersion, true);
if(!forgeVersion.isEmpty())
{
// FIXME: dirty, nasty, hack. Proper solution requires dependency resolution and knowledge of the metadata.
if(forgeVersion == "recommended")
{
if(forgemap.contains(mcVersion))
{
forgeVersion = forgemap[mcVersion];
}
else
{
logWarning(tr("Could not map recommended Forge version for Minecraft %1").arg(mcVersion));
}
}
components->setComponentVersion("net.minecraftforge", forgeVersion);
}
if(!fabricVersion.isEmpty())
{
components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion);
}
if (m_instIcon != "default")
{
instance.setIconKey(m_instIcon);
}
else
{
if(pack.name.contains("Direwolf20"))
{
instance.setIconKey("steve");
}
else if(pack.name.contains("FTB") || pack.name.contains("Feed The Beast"))
{
instance.setIconKey("ftb_logo");
}
else
{
// default to something other than the MultiMC default to distinguish these
instance.setIconKey("flame");
}
}
QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods");
QFileInfo jarmodsInfo(jarmodsPath);
if(jarmodsInfo.isDir())
{
// install all the jar mods
qDebug() << "Found jarmods:";
QDir jarmodsDir(jarmodsPath);
QStringList jarMods;
for (auto info: jarmodsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files))
{
qDebug() << info.fileName();
jarMods.push_back(info.absoluteFilePath());
}
auto profile = instance.getPackProfile();
profile->installJarMods(jarMods);
// nuke the original files
FS::deletePath(jarmodsPath);
}
instance.setName(m_instName);
m_modIdResolver = new Flame::FileResolvingTask(APPLICATION->network(), pack);
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]()
{
auto results = m_modIdResolver->getResults();
//first check for blocked mods
QString text;
auto anyBlocked = false;
for(const auto& result: results.files.values()) {
if (!result.resolved || result.url.isEmpty()) {
text += QString("%1: <a href='%2'>%2</a><br/>").arg(result.fileName, result.websiteUrl);
anyBlocked = true;
}
}
if(anyBlocked) {
qWarning() << "Blocked mods found, displaying mod list";
auto message_dialog = new ScrollMessageBox(m_parent,
tr("Blocked mods found"),
tr("The following mods were blocked on third party launchers.<br/>"
"You will need to manually download them and add them to the modpack"),
text);
message_dialog->setModal(true);
if (message_dialog->exec()) {
m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
for (const auto &result: m_modIdResolver->getResults().files) {
QString filename = result.fileName;
if (!result.required) {
filename += ".disabled";
}
auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
auto path = FS::PathCombine(m_stagingPath, relpath);
switch (result.type) {
case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
// fall-through intentional, we treat these as plain old mods and dump them wherever.
}
case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: {
if (!result.url.isEmpty()) {
qDebug() << "Will download" << result.url << "to" << path;
auto dl = Net::Download::makeFile(result.url, path);
m_filesNetJob->addNetAction(dl);
}
break;
}
case Flame::File::Type::Modpack:
logWarning(
tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(
relpath));
break;
case Flame::File::Type::Cmod2:
case Flame::File::Type::Ctoc:
case Flame::File::Type::Unknown:
logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
break;
}
}
m_modIdResolver.reset();
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() {
m_filesNetJob.reset();
emitSucceeded(); emitSucceeded();
}
);
connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) {
m_filesNetJob.reset();
emitFailed(reason);
}); });
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) { connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
setProgress(current, total); connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress);
}); connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus);
setStatus(tr("Downloading mods...")); connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);
m_filesNetJob->start();
} else {
m_modIdResolver.reset();
emitFailed("Canceled");
}
} else {
//TODO extract to function ?
m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
for (const auto &result: m_modIdResolver->getResults().files) {
QString filename = result.fileName;
if (!result.required) {
filename += ".disabled";
}
auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename); connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort);
auto path = FS::PathCombine(m_stagingPath, relpath); connect(inst_creation_task, &Task::aborted, this, &Task::abort);
connect(inst_creation_task, &Task::abortStatusChanged, this, &Task::setAbortable);
switch (result.type) { inst_creation_task->start();
case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
// fall-through intentional, we treat these as plain old mods and dump them wherever.
}
case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: {
if (!result.url.isEmpty()) {
qDebug() << "Will download" << result.url << "to" << path;
auto dl = Net::Download::makeFile(result.url, path);
m_filesNetJob->addNetAction(dl);
}
break;
}
case Flame::File::Type::Modpack:
logWarning(
tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(
relpath));
break;
case Flame::File::Type::Cmod2:
case Flame::File::Type::Ctoc:
case Flame::File::Type::Unknown:
logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
break;
}
}
m_modIdResolver.reset();
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() {
m_filesNetJob.reset();
emitSucceeded();
}
);
connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) {
m_filesNetJob.reset();
emitFailed(reason);
});
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
setProgress(current, total);
});
setStatus(tr("Downloading mods..."));
m_filesNetJob->start();
}
}
);
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason)
{
m_modIdResolver.reset();
emitFailed(tr("Unable to resolve mod IDs:\n") + reason);
});
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress, [&](qint64 current, qint64 total)
{
setProgress(current, total);
});
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status, [&](QString status)
{
setStatus(status);
});
m_modIdResolver->start();
} }
void InstanceImportTask::processTechnic() void InstanceImportTask::processTechnic()
@ -546,7 +290,7 @@ void InstanceImportTask::processTechnic()
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor(); shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded); connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed); connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed);
packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath); packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath);
} }
void InstanceImportTask::processMultiMC() void InstanceImportTask::processMultiMC()
@ -560,7 +304,7 @@ void InstanceImportTask::processMultiMC()
instance.resetTimePlayed(); instance.resetTimePlayed();
// set a new nice name // set a new nice name
instance.setName(m_instName); instance.setName(name());
// if the icon was specified by user, use that. otherwise pull icon from the pack // if the icon was specified by user, use that. otherwise pull icon from the pack
if (m_instIcon != "default") { if (m_instIcon != "default") {
@ -581,198 +325,26 @@ void InstanceImportTask::processMultiMC()
emitSucceeded(); emitSucceeded();
} }
// https://docs.modrinth.com/docs/modpacks/format_definition/
void InstanceImportTask::processModrinth() void InstanceImportTask::processModrinth()
{ {
std::vector<Modrinth::File> files; auto* inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, m_sourceUrl.toString());
QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion;
try {
QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json");
auto doc = Json::requireDocument(indexPath);
auto obj = Json::requireObject(doc, "modrinth.index.json");
int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json");
if (formatVersion == 1) {
auto game = Json::requireString(obj, "game", "modrinth.index.json");
if (game != "minecraft") {
throw JSONValidationError("Unknown game: " + game);
}
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json"); inst_creation_task->setName(*this);
bool had_optional = false; inst_creation_task->setIcon(m_instIcon);
for (auto modInfo : jsonFiles) { inst_creation_task->setGroup(m_instGroup);
Modrinth::File file;
file.path = Json::requireString(modInfo, "path");
auto env = Json::ensureObject(modInfo, "env"); connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] {
// 'env' field is optional setOverride(inst_creation_task->shouldOverride());
if (!env.isEmpty()) {
QString support = Json::ensureString(env, "client", "unsupported");
if (support == "unsupported") {
continue;
} else if (support == "optional") {
// TODO: Make a review dialog for choosing which ones the user wants!
if (!had_optional) {
had_optional = true;
auto info = CustomMessageBox::selectable(
m_parent, tr("Optional mod detected!"),
tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"),
QMessageBox::Information);
info->exec();
}
if (file.path.endsWith(".jar"))
file.path += ".disabled";
}
}
QJsonObject hashes = Json::requireObject(modInfo, "hashes");
QString hash;
QCryptographicHash::Algorithm hashAlgorithm;
hash = Json::ensureString(hashes, "sha1");
hashAlgorithm = QCryptographicHash::Sha1;
if (hash.isEmpty()) {
hash = Json::ensureString(hashes, "sha512");
hashAlgorithm = QCryptographicHash::Sha512;
if (hash.isEmpty()) {
hash = Json::ensureString(hashes, "sha256");
hashAlgorithm = QCryptographicHash::Sha256;
if (hash.isEmpty()) {
throw JSONValidationError("No hash found for: " + file.path);
}
}
}
file.hash = QByteArray::fromHex(hash.toLatin1());
file.hashAlgorithm = hashAlgorithm;
// Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
// (as Modrinth seems to incorrectly handle spaces)
auto download_arr = Json::ensureArray(modInfo, "downloads");
for(auto download : download_arr) {
qWarning() << download.toString();
bool is_last = download.toString() == download_arr.last().toString();
auto download_url = QUrl(download.toString());
if (!download_url.isValid()) {
qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL")
.arg(download_url.toString(), file.path);
if(is_last && file.downloads.isEmpty())
throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path));
}
else {
file.downloads.push_back(download_url);
}
}
files.push_back(file);
}
auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json");
for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) {
QString name = it.key();
if (name == "minecraft") {
minecraftVersion = Json::requireString(*it, "Minecraft version");
}
else if (name == "fabric-loader") {
fabricVersion = Json::requireString(*it, "Fabric Loader version");
}
else if (name == "quilt-loader") {
quiltVersion = Json::requireString(*it, "Quilt Loader version");
}
else if (name == "forge") {
forgeVersion = Json::requireString(*it, "Forge version");
}
else {
throw JSONValidationError("Unknown dependency type: " + name);
}
}
} else {
throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion));
}
QFile::remove(indexPath);
} catch (const JSONValidationError& e) {
emitFailed(tr("Could not understand pack index:\n") + e.cause());
return;
}
auto mcPath = FS::PathCombine(m_stagingPath, ".minecraft");
auto override_path = FS::PathCombine(m_stagingPath, "overrides");
if (QFile::exists(override_path)) {
if (!QFile::rename(override_path, mcPath)) {
emitFailed(tr("Could not rename the overrides folder:\n") + "overrides");
return;
}
}
// Do client overrides
auto client_override_path = FS::PathCombine(m_stagingPath, "client-overrides");
if (QFile::exists(client_override_path)) {
if (!FS::overrideFolder(mcPath, client_override_path)) {
emitFailed(tr("Could not rename the client overrides folder:\n") + "client overrides");
return;
}
}
QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
auto components = instance.getPackProfile();
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", minecraftVersion, true);
if (!fabricVersion.isEmpty())
components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion);
if (!quiltVersion.isEmpty())
components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion);
if (!forgeVersion.isEmpty())
components->setComponentVersion("net.minecraftforge", forgeVersion);
if (m_instIcon != "default")
{
instance.setIconKey(m_instIcon);
}
else
{
instance.setIconKey("modrinth");
}
instance.setName(m_instName);
instance.saveNow();
m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
for (auto file : files)
{
auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path);
qDebug() << "Will try to download" << file.downloads.front() << "to" << path;
auto dl = Net::Download::makeFile(file.downloads.dequeue(), path);
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_filesNetJob->addNetAction(dl);
if (file.downloads.size() > 0) {
// FIXME: This really needs to be put into a ConcurrentTask of
// MultipleOptionsTask's , once those exist :)
connect(dl.get(), &NetAction::failed, [this, &file, path, dl]{
auto dl = Net::Download::makeFile(file.downloads.dequeue(), path);
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_filesNetJob->addNetAction(dl);
dl->succeeded();
});
}
}
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]()
{
m_filesNetJob.reset();
emitSucceeded(); emitSucceeded();
}
);
connect(m_filesNetJob.get(), &NetJob::failed, [&](const QString &reason)
{
m_filesNetJob.reset();
emitFailed(reason);
}); });
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
{ connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress);
setProgress(current, total); connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus);
}); connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);
setStatus(tr("Downloading mods..."));
m_filesNetJob->start(); connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort);
connect(inst_creation_task, &Task::aborted, this, &Task::abort);
connect(inst_creation_task, &Task::abortStatusChanged, this, &Task::setAbortable);
inst_creation_task->start();
} }

View File

@ -44,7 +44,7 @@
#include "QObjectPtr.h" #include "QObjectPtr.h"
#include "modplatform/flame/PackManifest.h" #include "modplatform/flame/PackManifest.h"
#include <nonstd/optional> #include <optional>
class QuaZip; class QuaZip;
namespace Flame namespace Flame
@ -58,7 +58,6 @@ class InstanceImportTask : public InstanceTask
public: public:
explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr); explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr);
bool canAbort() const override { return true; }
bool abort() override; bool abort() override;
const QVector<Flame::File> &getBlockedFiles() const const QVector<Flame::File> &getBlockedFiles() const
{ {
@ -80,6 +79,7 @@ private slots:
void downloadSucceeded(); void downloadSucceeded();
void downloadFailed(QString reason); void downloadFailed(QString reason);
void downloadProgressChanged(qint64 current, qint64 total); void downloadProgressChanged(qint64 current, qint64 total);
void downloadAborted();
void extractFinished(); void extractFinished();
void extractAborted(); void extractAborted();
@ -90,8 +90,8 @@ private: /* data */
QString m_archivePath; QString m_archivePath;
bool m_downloadRequired = false; bool m_downloadRequired = false;
std::unique_ptr<QuaZip> m_packZip; std::unique_ptr<QuaZip> m_packZip;
QFuture<nonstd::optional<QStringList>> m_extractFuture; QFuture<std::optional<QStringList>> m_extractFuture;
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher; QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
QVector<Flame::File> m_blockedMods; QVector<Flame::File> m_blockedMods;
enum class ModpackType{ enum class ModpackType{
Unknown, Unknown,

View File

@ -33,30 +33,32 @@
* limitations under the License. * limitations under the License.
*/ */
#include <QDebug>
#include <QDir> #include <QDir>
#include <QDirIterator> #include <QDirIterator>
#include <QSet>
#include <QFile> #include <QFile>
#include <QThread>
#include <QTextStream>
#include <QXmlStreamReader>
#include <QTimer>
#include <QDebug>
#include <QFileSystemWatcher> #include <QFileSystemWatcher>
#include <QUuid>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QMimeData> #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 "BaseInstance.h"
#include "InstanceTask.h"
#include "settings/INISettingsObject.h"
#include "NullInstance.h"
#include "minecraft/MinecraftInstance.h"
#include "FileSystem.h"
#include "ExponentialSeries.h" #include "ExponentialSeries.h"
#include "FileSystem.h"
#include "InstanceList.h"
#include "InstanceTask.h"
#include "NullInstance.h"
#include "WatchLock.h" #include "WatchLock.h"
#include "minecraft/MinecraftInstance.h"
#include "settings/INISettingsObject.h"
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
#include <Windows.h> #include <Windows.h>
@ -69,8 +71,7 @@ InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir,
{ {
resumeWatch(); resumeWatch();
// Create aand normalize path // Create aand normalize path
if (!QDir::current().exists(instDir)) if (!QDir::current().exists(instDir)) {
{
QDir::current().mkpath(instDir); QDir::current().mkpath(instDir);
} }
@ -83,9 +84,7 @@ InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir,
m_watcher->addPath(m_instDir); m_watcher->addPath(m_instDir);
} }
InstanceList::~InstanceList() InstanceList::~InstanceList() {}
{
}
Qt::DropActions InstanceList::supportedDragActions() const Qt::DropActions InstanceList::supportedDragActions() const
{ {
@ -130,7 +129,6 @@ QMimeData * InstanceList::mimeData(const QModelIndexList& indexes) const
return mimeData; return mimeData;
} }
int InstanceList::rowCount(const QModelIndex& parent) const int InstanceList::rowCount(const QModelIndex& parent) const
{ {
Q_UNUSED(parent); Q_UNUSED(parent);
@ -147,8 +145,7 @@ QModelIndex InstanceList::index(int row, int column, const QModelIndex &parent)
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(); return QVariant();
} }
BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer()); BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer());
@ -193,18 +190,15 @@ QVariant InstanceList::data(const QModelIndex &index, int role) const
bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role) bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role)
{ {
if (!index.isValid()) if (!index.isValid()) {
{
return false; return false;
} }
if(role != Qt::EditRole) if (role != Qt::EditRole) {
{
return false; return false;
} }
BaseInstance* pdata = static_cast<BaseInstance*>(index.internalPointer()); BaseInstance* pdata = static_cast<BaseInstance*>(index.internalPointer());
auto newName = value.toString(); auto newName = value.toString();
if(pdata->name() == newName) if (pdata->name() == newName) {
{
return true; return true;
} }
pdata->setName(newName); pdata->setName(newName);
@ -214,8 +208,7 @@ bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int
Qt::ItemFlags InstanceList::flags(const QModelIndex& index) const Qt::ItemFlags InstanceList::flags(const QModelIndex& index) const
{ {
Qt::ItemFlags f; Qt::ItemFlags f;
if (index.isValid()) if (index.isValid()) {
{
f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
} }
return f; return f;
@ -224,13 +217,11 @@ Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const
GroupId InstanceList::getInstanceGroup(const InstanceId& id) const GroupId InstanceList::getInstanceGroup(const InstanceId& id) const
{ {
auto inst = getInstanceById(id); auto inst = getInstanceById(id);
if(!inst) if (!inst) {
{
return GroupId(); return GroupId();
} }
auto iter = m_instanceGroupIndex.find(inst->id()); auto iter = m_instanceGroupIndex.find(inst->id());
if(iter != m_instanceGroupIndex.end()) if (iter != m_instanceGroupIndex.end()) {
{
return *iter; return *iter;
} }
return GroupId(); return GroupId();
@ -239,30 +230,24 @@ GroupId InstanceList::getInstanceGroup(const InstanceId& id) const
void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name) void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name)
{ {
auto inst = getInstanceById(id); auto inst = getInstanceById(id);
if(!inst) if (!inst) {
{
qDebug() << "Attempt to set a null instance's group"; qDebug() << "Attempt to set a null instance's group";
return; return;
} }
bool changed = false; bool changed = false;
auto iter = m_instanceGroupIndex.find(inst->id()); auto iter = m_instanceGroupIndex.find(inst->id());
if(iter != m_instanceGroupIndex.end()) if (iter != m_instanceGroupIndex.end()) {
{ if (*iter != name) {
if(*iter != name)
{
*iter = name; *iter = name;
changed = true; changed = true;
} }
} } else {
else
{
changed = true; changed = true;
m_instanceGroupIndex[id] = name; m_instanceGroupIndex[id] = name;
} }
if(changed) if (changed) {
{
m_groupNameCache.insert(name); m_groupNameCache.insert(name);
auto idx = getInstIndex(inst.get()); auto idx = getInstIndex(inst.get());
emit dataChanged(index(idx), index(idx), { GroupRole }); emit dataChanged(index(idx), index(idx), { GroupRole });
@ -279,24 +264,20 @@ void InstanceList::deleteGroup(const QString& name)
{ {
bool removed = false; bool removed = false;
qDebug() << "Delete group" << name; qDebug() << "Delete group" << name;
for(auto & instance: m_instances) for (auto& instance : m_instances) {
{
const auto& instID = instance->id(); const auto& instID = instance->id();
auto instGroupName = getInstanceGroup(instID); auto instGroupName = getInstanceGroup(instID);
if(instGroupName == name) if (instGroupName == name) {
{
m_instanceGroupIndex.remove(instID); m_instanceGroupIndex.remove(instID);
qDebug() << "Remove" << instID << "from group" << name; qDebug() << "Remove" << instID << "from group" << name;
removed = true; removed = true;
auto idx = getInstIndex(instance.get()); auto idx = getInstIndex(instance.get());
if(idx > 0) if (idx > 0) {
{
emit dataChanged(index(idx), index(idx), { GroupRole }); emit dataChanged(index(idx), index(idx), { GroupRole });
} }
} }
} }
if(removed) if (removed) {
{
saveGroupList(); saveGroupList();
} }
} }
@ -306,23 +287,75 @@ bool InstanceList::isGroupCollapsed(const QString& group)
return m_collapsedGroups.contains(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) void InstanceList::deleteInstance(const InstanceId& id)
{ {
auto inst = getInstanceById(id); auto inst = getInstanceById(id);
if(!inst) if (!inst) {
{
qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?).";
return; return;
} }
if(m_instanceGroupIndex.remove(id)) if (m_instanceGroupIndex.remove(id)) {
{
saveGroupList(); saveGroupList();
} }
qDebug() << "Will delete instance" << id; 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 ..."; qWarning() << "Deletion of instance" << id << "has not been completely successful ...";
return; return;
} }
@ -334,11 +367,9 @@ static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr> &
{ {
QMap<InstanceId, InstanceLocator> out; QMap<InstanceId, InstanceLocator> out;
int i = 0; int i = 0;
for(auto & item: list) for (auto& item : list) {
{
auto id = item->id(); auto id = item->id();
if(out.contains(id)) if (out.contains(id)) {
{
qWarning() << "Duplicate ID" << id << "in instance list"; qWarning() << "Duplicate ID" << id << "in instance list";
} }
out[id] = std::make_pair(item, i); out[id] = std::make_pair(item, i);
@ -352,19 +383,16 @@ QList< InstanceId > InstanceList::discoverInstances()
qDebug() << "Discovering instances in" << m_instDir; qDebug() << "Discovering instances in" << m_instDir;
QList<InstanceId> out; QList<InstanceId> out;
QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); 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(); QString subDir = iter.next();
QFileInfo dirInfo(subDir); QFileInfo dirInfo(subDir);
if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists()) if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists())
continue; continue;
// if it is a symlink, ignore it if it goes to the instance folder // 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 targetInfo(dirInfo.symLinkTarget());
QFileInfo instDirInfo(m_instDir); QFileInfo instDirInfo(m_instDir);
if(targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) if (targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) {
{
qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder"; qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder";
continue; continue;
} }
@ -388,74 +416,56 @@ InstanceList::InstListError InstanceList::loadList()
QList<InstancePtr> newList; QList<InstancePtr> newList;
for(auto & id: discoverInstances()) for (auto& id : discoverInstances()) {
{ if (existingIds.contains(id)) {
if(existingIds.contains(id))
{
auto instPair = existingIds[id]; auto instPair = existingIds[id];
existingIds.remove(id); existingIds.remove(id);
qDebug() << "Should keep and soft-reload" << id; qDebug() << "Should keep and soft-reload" << id;
} } else {
else
{
InstancePtr instPtr = loadInstance(id); InstancePtr instPtr = loadInstance(id);
if(instPtr) if (instPtr) {
{
newList.append(instPtr); newList.append(instPtr);
} }
} }
} }
// TODO: looks like a general algorithm with a few specifics inserted. Do something about it. // 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 // get the list of removed instances and sort it by their original index, from last to first
auto deadList = existingIds.values(); auto deadList = existingIds.values();
auto orderSortPredicate = [](const InstanceLocator & a, const InstanceLocator & b) -> bool auto orderSortPredicate = [](const InstanceLocator& a, const InstanceLocator& b) -> bool { return a.second > b.second; };
{
return a.second > b.second;
};
std::sort(deadList.begin(), deadList.end(), orderSortPredicate); std::sort(deadList.begin(), deadList.end(), orderSortPredicate);
// remove the contiguous ranges of rows // remove the contiguous ranges of rows
int front_bookmark = -1; int front_bookmark = -1;
int back_bookmark = -1; int back_bookmark = -1;
int currentItem = -1; int currentItem = -1;
auto removeNow = [&]() auto removeNow = [&]() {
{
beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark); beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark);
m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1); m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1);
endRemoveRows(); endRemoveRows();
front_bookmark = -1; front_bookmark = -1;
back_bookmark = currentItem; back_bookmark = currentItem;
}; };
for(auto & removedItem: deadList) for (auto& removedItem : deadList) {
{
auto instPtr = removedItem.first; auto instPtr = removedItem.first;
instPtr->invalidate(); instPtr->invalidate();
currentItem = removedItem.second; currentItem = removedItem.second;
if(back_bookmark == -1) if (back_bookmark == -1) {
{
// no bookmark yet // no bookmark yet
back_bookmark = currentItem; back_bookmark = currentItem;
} } else if (currentItem == front_bookmark - 1) {
else if(currentItem == front_bookmark - 1)
{
// part of contiguous sequence, continue // part of contiguous sequence, continue
} } else {
else
{
// seam between previous and current item // seam between previous and current item
removeNow(); removeNow();
} }
front_bookmark = currentItem; front_bookmark = currentItem;
} }
if(back_bookmark != -1) if (back_bookmark != -1) {
{
removeNow(); removeNow();
} }
} }
if(newList.size()) if (newList.size()) {
{
add(newList); add(newList);
} }
m_dirty = false; m_dirty = false;
@ -466,16 +476,14 @@ InstanceList::InstListError InstanceList::loadList()
void InstanceList::updateTotalPlayTime() void InstanceList::updateTotalPlayTime()
{ {
totalPlayTime = 0; totalPlayTime = 0;
for(auto const& itr : m_instances) for (auto const& itr : m_instances) {
{
totalPlayTime += itr.get()->totalTimePlayed(); totalPlayTime += itr.get()->totalTimePlayed();
} }
} }
void InstanceList::saveNow() void InstanceList::saveNow()
{ {
for(auto & item: m_instances) for (auto& item : m_instances) {
{
item->saveNow(); item->saveNow();
} }
} }
@ -484,8 +492,7 @@ void InstanceList::add(const QList<InstancePtr> &t)
{ {
beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1); beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1);
m_instances.append(t); m_instances.append(t);
for(auto & ptr : t) for (auto& ptr : t) {
{
connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged);
} }
endInsertRows(); endInsertRows();
@ -493,14 +500,12 @@ void InstanceList::add(const QList<InstancePtr> &t)
void InstanceList::resumeWatch() void InstanceList::resumeWatch()
{ {
if(m_watchLevel > 0) if (m_watchLevel > 0) {
{
qWarning() << "Bad suspend level resume in instance list"; qWarning() << "Bad suspend level resume in instance list";
return; return;
} }
m_watchLevel++; m_watchLevel++;
if(m_watchLevel > 0 && m_dirty) if (m_watchLevel > 0 && m_dirty) {
{
loadList(); loadList();
} }
} }
@ -513,8 +518,7 @@ void InstanceList::suspendWatch()
void InstanceList::providerUpdated() void InstanceList::providerUpdated()
{ {
m_dirty = true; m_dirty = true;
if(m_watchLevel == 1) if (m_watchLevel == 1) {
{
loadList(); loadList();
} }
} }
@ -523,16 +527,27 @@ InstancePtr InstanceList::getInstanceById(QString instId) const
{ {
if (instId.isEmpty()) if (instId.isEmpty())
return InstancePtr(); return InstancePtr();
for(auto & inst: m_instances) for (auto& inst : m_instances) {
{ if (inst->id() == instId) {
if (inst->id() == instId)
{
return inst; return inst;
} }
} }
return InstancePtr(); return InstancePtr();
} }
InstancePtr InstanceList::getInstanceByManagedName(const QString& managed_name) const
{
if (managed_name.isEmpty())
return {};
for (auto instance : m_instances) {
if (instance->getManagedPackName() == managed_name)
return instance;
}
return {};
}
QModelIndex InstanceList::getInstanceIndexById(const QString &id) const QModelIndex InstanceList::getInstanceIndexById(const QString &id) const
{ {
return index(getInstIndex(getInstanceById(id).get())); return index(getInstIndex(getInstanceById(id).get()));
@ -541,10 +556,8 @@ QModelIndex InstanceList::getInstanceIndexById(const QString &id) const
int InstanceList::getInstIndex(BaseInstance* inst) const int InstanceList::getInstIndex(BaseInstance* inst) const
{ {
int count = m_instances.count(); int count = m_instances.count();
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++) {
{ if (inst == m_instances[i].get()) {
if (inst == m_instances[i].get())
{
return i; return i;
} }
} }
@ -554,8 +567,7 @@ int InstanceList::getInstIndex(BaseInstance *inst) const
void InstanceList::propertiesChanged(BaseInstance* inst) void InstanceList::propertiesChanged(BaseInstance* inst)
{ {
int i = getInstIndex(inst); int i = getInstIndex(inst);
if (i != -1) if (i != -1) {
{
emit dataChanged(index(i), index(i)); emit dataChanged(index(i), index(i));
updateTotalPlayTime(); updateTotalPlayTime();
} }
@ -563,8 +575,7 @@ void InstanceList::propertiesChanged(BaseInstance *inst)
InstancePtr InstanceList::loadInstance(const InstanceId& id) InstancePtr InstanceList::loadInstance(const InstanceId& id)
{ {
if(!m_groupsLoaded) if (!m_groupsLoaded) {
{
loadGroupList(); loadGroupList();
} }
@ -592,34 +603,28 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id)
void InstanceList::saveGroupList() void InstanceList::saveGroupList()
{ {
qDebug() << "Will save group list now."; 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."; qDebug() << "Group saving prevented because we don't know the full list of instances yet.";
return; return;
} }
WatchLock foo(m_watcher, m_instDir); WatchLock foo(m_watcher, m_instDir);
QString groupFileName = m_instDir + "/instgroups.json"; QString groupFileName = m_instDir + "/instgroups.json";
QMap<QString, QSet<QString>> reverseGroupMap; 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 id = iter.key();
QString group = iter.value(); QString group = iter.value();
if (group.isEmpty()) if (group.isEmpty())
continue; continue;
if(!instanceSet.contains(id)) if (!instanceSet.contains(id)) {
{
qDebug() << "Skipping saving missing instance" << id << "to groups list."; qDebug() << "Skipping saving missing instance" << id << "to groups list.";
continue; continue;
} }
if (!reverseGroupMap.count(group)) if (!reverseGroupMap.count(group)) {
{
QSet<QString> set; QSet<QString> set;
set.insert(id); set.insert(id);
reverseGroupMap[group] = set; reverseGroupMap[group] = set;
} } else {
else
{
QSet<QString>& set = reverseGroupMap[group]; QSet<QString>& set = reverseGroupMap[group];
set.insert(id); set.insert(id);
} }
@ -627,15 +632,13 @@ void InstanceList::saveGroupList()
QJsonObject toplevel; QJsonObject toplevel;
toplevel.insert("formatVersion", QJsonValue(QString("1"))); toplevel.insert("formatVersion", QJsonValue(QString("1")));
QJsonObject groupsArr; 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 list = iter.value();
auto name = iter.key(); auto name = iter.key();
QJsonObject groupObj; QJsonObject groupObj;
QJsonArray instanceArr; QJsonArray instanceArr;
groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name))); groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name)));
for (auto item : list) for (auto item : list) {
{
instanceArr.append(QJsonValue(item)); instanceArr.append(QJsonValue(item));
} }
groupObj.insert("instances", instanceArr); groupObj.insert("instances", instanceArr);
@ -643,13 +646,10 @@ void InstanceList::saveGroupList()
} }
toplevel.insert("groups", groupsArr); toplevel.insert("groups", groupsArr);
QJsonDocument doc(toplevel); QJsonDocument doc(toplevel);
try try {
{
FS::write(groupFileName, doc.toJson()); FS::write(groupFileName, doc.toJson());
qDebug() << "Group list saved."; qDebug() << "Group list saved.";
} } catch (const FS::FileSystemException& e) {
catch (const FS::FileSystemException &e)
{
qCritical() << "Failed to write instance group file :" << e.cause(); qCritical() << "Failed to write instance group file :" << e.cause();
} }
} }
@ -665,12 +665,9 @@ void InstanceList::loadGroupList()
return; return;
QByteArray jsonData; QByteArray jsonData;
try try {
{
jsonData = FS::read(groupFileName); jsonData = FS::read(groupFileName);
} } catch (const FS::FileSystemException& e) {
catch (const FS::FileSystemException &e)
{
qCritical() << "Failed to read instance group file :" << e.cause(); qCritical() << "Failed to read instance group file :" << e.cause();
return; return;
} }
@ -679,8 +676,7 @@ void InstanceList::loadGroupList()
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error); QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error);
// if the json was bad, fail // 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") qCritical() << QString("Failed to parse instance group file: %1 at offset %2")
.arg(error.errorString(), QString::number(error.offset)) .arg(error.errorString(), QString::number(error.offset))
.toUtf8(); .toUtf8();
@ -688,8 +684,7 @@ void InstanceList::loadGroupList()
} }
// if the root of the json wasn't an object, fail // 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."; qWarning() << "Invalid group file. Root entry should be an object.";
return; return;
} }
@ -701,8 +696,7 @@ void InstanceList::loadGroupList()
return; return;
// Get the groups. if it's not an object, fail // 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."; qWarning() << "Invalid group list JSON: 'groups' should be an object.";
return; return;
} }
@ -712,21 +706,20 @@ void InstanceList::loadGroupList()
// Iterate through all the groups. // Iterate through all the groups.
QJsonObject groupMapping = rootObj.value("groups").toObject(); 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(); QString groupName = iter.key();
// If not an object, complain and skip to the next one. // 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(); qWarning() << QString("Group '%1' in the group list should be an object.").arg(groupName).toUtf8();
continue; continue;
} }
QJsonObject groupObj = iter.value().toObject(); QJsonObject groupObj = iter.value().toObject();
if (!groupObj.value("instances").isArray()) if (!groupObj.value("instances").isArray()) {
{ qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.")
qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.").arg(groupName).toUtf8(); .arg(groupName)
.toUtf8();
continue; continue;
} }
@ -741,8 +734,7 @@ void InstanceList::loadGroupList()
// Iterate through the list of instances in the group. // Iterate through the list of instances in the group.
QJsonArray instancesArray = groupObj.value("instances").toArray(); 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; m_instanceGroupIndex[(*iter2).toString()] = groupName;
} }
} }
@ -760,10 +752,8 @@ void InstanceList::instanceDirContentsChanged(const QString& path)
void InstanceList::on_InstFolderChanged(const Setting& setting, QVariant value) void InstanceList::on_InstFolderChanged(const Setting& setting, QVariant value)
{ {
QString newInstDir = QDir(value.toString()).canonicalPath(); QString newInstDir = QDir(value.toString()).canonicalPath();
if(newInstDir != m_instDir) if (newInstDir != m_instDir) {
{ if (m_groupsLoaded) {
if(m_groupsLoaded)
{
saveGroupList(); saveGroupList();
} }
m_instDir = newInstDir; m_instDir = newInstDir;
@ -783,80 +773,60 @@ void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed)
saveGroupList(); saveGroupList();
} }
class InstanceStaging : public Task class InstanceStaging : public Task {
{
Q_OBJECT Q_OBJECT
const unsigned minBackoff = 1; const unsigned minBackoff = 1;
const unsigned maxBackoff = 16; const unsigned maxBackoff = 16;
public: public:
InstanceStaging ( InstanceStaging(InstanceList* parent, InstanceTask* child, QString stagingPath, InstanceName const& instanceName, QString groupName)
InstanceList * parent, : m_parent(parent), backoff(minBackoff, maxBackoff), m_stagingPath(std::move(stagingPath)), m_instance_name(std::move(instanceName)), m_groupName(std::move(groupName))
Task * child,
const QString & stagingPath,
const QString& instanceName,
const QString& groupName )
: backoff(minBackoff, maxBackoff)
{ {
m_parent = parent;
m_child.reset(child); m_child.reset(child);
connect(child, &Task::succeeded, this, &InstanceStaging::childSucceded); connect(child, &Task::succeeded, this, &InstanceStaging::childSucceded);
connect(child, &Task::failed, this, &InstanceStaging::childFailed); connect(child, &Task::failed, this, &InstanceStaging::childFailed);
connect(child, &Task::aborted, this, &InstanceStaging::childAborted);
connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable);
connect(child, &Task::status, this, &InstanceStaging::setStatus); connect(child, &Task::status, this, &InstanceStaging::setStatus);
connect(child, &Task::progress, this, &InstanceStaging::setProgress); connect(child, &Task::progress, this, &InstanceStaging::setProgress);
m_instanceName = instanceName;
m_groupName = groupName;
m_stagingPath = stagingPath;
m_backoffTimer.setSingleShot(true);
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded); connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded);
} }
virtual ~InstanceStaging(){}; virtual ~InstanceStaging(){};
// FIXME/TODO: add ability to abort during instance commit retries // FIXME/TODO: add ability to abort during instance commit retries
bool abort() override bool abort() override
{ {
if(m_child && m_child->canAbort()) if (!canAbort())
{
return m_child->abort();
}
return false; return false;
m_child->abort();
return Task::abort();
} }
bool canAbort() const override bool canAbort() const override
{ {
if(m_child && m_child->canAbort()) return (m_child && m_child->canAbort());
{
return true;
}
return false;
} }
protected: protected:
virtual void executeTask() override virtual void executeTask() override { m_child->start(); }
{ QStringList warnings() const override { return m_child->warnings(); }
m_child->start();
}
QStringList warnings() const override
{
return m_child->warnings();
}
private slots: private slots:
void childSucceded() void childSucceded()
{ {
unsigned sleepTime = backoff(); unsigned sleepTime = backoff();
if(m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName)) if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, m_child->shouldOverride()))
{ {
emitSucceeded(); emitSucceeded();
return; return;
} }
// we actually failed, retry? // 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.")); emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something."));
return; return;
} }
qDebug() << "Failed to commit instance" << m_instanceName << "Initiating backoff:" << sleepTime; qDebug() << "Failed to commit instance" << m_instance_name.name() << "Initiating backoff:" << sleepTime;
m_backoffTimer.start(sleepTime * 500); m_backoffTimer.start(sleepTime * 500);
} }
void childFailed(const QString& reason) void childFailed(const QString& reason)
@ -865,7 +835,13 @@ private slots:
emitFailed(reason); emitFailed(reason);
} }
void childAborted()
{
emitAborted();
}
private: private:
InstanceList * m_parent;
/* /*
* WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows. * 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 * Basically, it starts messing things up while the launcher is extracting/creating instances
@ -873,9 +849,8 @@ private:
*/ */
ExponentialSeries backoff; ExponentialSeries backoff;
QString m_stagingPath; QString m_stagingPath;
InstanceList * m_parent; unique_qobject_ptr<InstanceTask> m_child;
unique_qobject_ptr<Task> m_child; InstanceName m_instance_name;
QString m_instanceName;
QString m_groupName; QString m_groupName;
QTimer m_backoffTimer; QTimer m_backoffTimer;
}; };
@ -885,7 +860,7 @@ Task * InstanceList::wrapInstanceTask(InstanceTask * task)
auto stagingPath = getStagedInstancePath(); auto stagingPath = getStagedInstancePath();
task->setStagingPath(stagingPath); task->setStagingPath(stagingPath);
task->setParentSettings(m_globalSettings); task->setParentSettings(m_globalSettings);
return new InstanceStaging(this, task, stagingPath, task->name(), task->group()); return new InstanceStaging(this, task, stagingPath, *task, task->group());
} }
QString InstanceList::getStagedInstancePath() QString InstanceList::getStagedInstancePath()
@ -895,8 +870,7 @@ QString InstanceList::getStagedInstancePath()
QString relPath = FS::PathCombine(tempDir, key); QString relPath = FS::PathCombine(tempDir, key);
QDir rootPath(m_instDir); QDir rootPath(m_instDir);
auto path = FS::PathCombine(m_instDir, relPath); auto path = FS::PathCombine(m_instDir, relPath);
if(!rootPath.mkpath(relPath)) if (!rootPath.mkpath(relPath)) {
{
return QString(); return QString();
} }
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
@ -906,24 +880,50 @@ QString InstanceList::getStagedInstancePath()
return path; return path;
} }
bool InstanceList::commitStagedInstance(const QString& path, const QString& instanceName, const QString& groupName) bool InstanceList::commitStagedInstance(const QString& path, InstanceName const& instanceName, const QString& groupName, bool should_override)
{ {
QDir dir; QDir dir;
QString instID = FS::DirNameFromString(instanceName, m_instDir); QString instID;
InstancePtr inst;
if (should_override) {
// This is to avoid problems when the instance folder gets manually renamed
if ((inst = getInstanceByManagedName(instanceName.originalName()))) {
instID = QFileInfo(inst->instanceRoot()).fileName();
} else if ((inst = getInstanceByManagedName(instanceName.modifiedName()))) {
instID = QFileInfo(inst->instanceRoot()).fileName();
} else {
instID = FS::RemoveInvalidFilenameChars(instanceName.modifiedName(), '-');
}
} else {
instID = FS::DirNameFromString(instanceName.modifiedName(), m_instDir);
}
{ {
WatchLock lock(m_watcher, m_instDir); WatchLock lock(m_watcher, m_instDir);
QString destination = FS::PathCombine(m_instDir, instID); QString destination = FS::PathCombine(m_instDir, instID);
if(!dir.rename(path, destination))
{ if (should_override) {
if (!FS::overrideFolder(destination, path)) {
qWarning() << "Failed to override" << path << "to" << destination;
return false;
}
} else {
if (!dir.rename(path, destination)) {
qWarning() << "Failed to move" << path << "to" << destination; qWarning() << "Failed to move" << path << "to" << destination;
return false; return false;
} }
m_instanceGroupIndex[instID] = groupName; m_instanceGroupIndex[instID] = groupName;
instanceSet.insert(instID);
m_groupNameCache.insert(groupName); m_groupNameCache.insert(groupName);
}
instanceSet.insert(instID);
emit instancesChanged(); emit instancesChanged();
emit instanceSelectRequest(instID); emit instanceSelectRequest(instID);
} }
saveGroupList(); saveGroupList();
return true; return true;
} }
@ -933,7 +933,8 @@ bool InstanceList::destroyStagingPath(const QString& keyPath)
return FS::deletePath(keyPath); return FS::deletePath(keyPath);
} }
int InstanceList::getTotalPlayTime() { int InstanceList::getTotalPlayTime()
{
updateTotalPlayTime(); updateTotalPlayTime();
return totalPlayTime; return totalPlayTime;
} }

View File

@ -19,13 +19,15 @@
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QSet> #include <QSet>
#include <QList> #include <QList>
#include <QStack>
#include <QPair>
#include "BaseInstance.h" #include "BaseInstance.h"
#include "QObjectPtr.h"
class QFileSystemWatcher; class QFileSystemWatcher;
class InstanceTask; class InstanceTask;
struct InstanceName;
using InstanceId = QString; using InstanceId = QString;
using GroupId = QString; using GroupId = QString;
using InstanceLocator = std::pair<InstancePtr, int>; using InstanceLocator = std::pair<InstancePtr, int>;
@ -46,6 +48,12 @@ enum class GroupsState
Dirty Dirty
}; };
struct TrashHistoryItem {
QString id;
QString polyPath;
QString trashPath;
QString groupName;
};
class InstanceList : public QAbstractListModel class InstanceList : public QAbstractListModel
{ {
@ -93,7 +101,10 @@ public:
InstListError loadList(); InstListError loadList();
void saveNow(); void saveNow();
/* O(n) */
InstancePtr getInstanceById(QString id) const; InstancePtr getInstanceById(QString id) const;
/* O(n) */
InstancePtr getInstanceByManagedName(const QString& managed_name) const;
QModelIndex getInstanceIndexById(const QString &id) const; QModelIndex getInstanceIndexById(const QString &id) const;
QStringList getGroups(); QStringList getGroups();
bool isGroupCollapsed(const QString &groupName); bool isGroupCollapsed(const QString &groupName);
@ -102,6 +113,9 @@ public:
void setInstanceGroup(const InstanceId & id, const GroupId& name); void setInstanceGroup(const InstanceId & id, const GroupId& name);
void deleteGroup(const GroupId & name); void deleteGroup(const GroupId & name);
bool trashInstance(const InstanceId &id);
bool trashedSomething();
void undoTrashInstance();
void deleteInstance(const InstanceId & id); void deleteInstance(const InstanceId & id);
// Wrap an instance creation task in some more task machinery and make it ready to be used // Wrap an instance creation task in some more task machinery and make it ready to be used
@ -116,8 +130,10 @@ public:
/** /**
* Commit the staging area given by @keyPath to the provider - used when creation succeeds. * Commit the staging area given by @keyPath to the provider - used when creation succeeds.
* Used by instance manipulation tasks. * Used by instance manipulation tasks.
* should_override is used when another similar instance already exists, and we want to override it
* - for instance, when updating it.
*/ */
bool commitStagedInstance(const QString & keyPath, const QString& instanceName, const QString & groupName); bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, const QString& groupName, bool should_override);
/** /**
* Destroy a previously created staging area given by @keyPath - used when creation fails. * Destroy a previously created staging area given by @keyPath - used when creation fails.
@ -180,4 +196,6 @@ private:
QSet<InstanceId> instanceSet; QSet<InstanceId> instanceSet;
bool m_groupsLoaded = false; bool m_groupsLoaded = false;
bool m_instancesProbed = false; bool m_instancesProbed = false;
QStack<TrashHistoryItem> m_trashHistory;
}; };

View File

@ -37,9 +37,9 @@ public:
modsPage->setFilter("%1 (*.zip *.jar *.litemod)"); modsPage->setFilter("%1 (*.zip *.jar *.litemod)");
values.append(modsPage); values.append(modsPage);
values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList())); values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList()));
values.append(new ResourcePackPage(onesix.get())); values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList()));
values.append(new TexturePackPage(onesix.get())); values.append(new TexturePackPage(onesix.get(), onesix->texturePackList()));
values.append(new ShaderPackPage(onesix.get())); values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList()));
values.append(new NotesPage(onesix.get())); values.append(new NotesPage(onesix.get()));
values.append(new WorldListPage(onesix.get(), onesix->worldList())); values.append(new WorldListPage(onesix.get(), onesix->worldList()));
values.append(new ServersPage(onesix)); values.append(new ServersPage(onesix));

View File

@ -1,9 +1,52 @@
#include "InstanceTask.h" #include "InstanceTask.h"
InstanceTask::InstanceTask() #include "ui/dialogs/CustomMessageBox.h"
InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name)
{ {
auto dialog =
CustomMessageBox::selectable(parent, QObject::tr("Change instance name"),
QObject::tr("The instance's name seems to include the old version. Would you like to update it?\n\n"
"Old name: %1\n"
"New name: %2")
.arg(old_name, new_name),
QMessageBox::Question, QMessageBox::No | QMessageBox::Yes);
auto result = dialog->exec();
if (result == QMessageBox::Yes)
return InstanceNameChange::ShouldChange;
return InstanceNameChange::ShouldKeep;
} }
InstanceTask::~InstanceTask() QString InstanceName::name() const
{ {
if (!m_modified_name.isEmpty())
return modifiedName();
return QString("%1 %2").arg(m_original_name, m_original_version);
} }
QString InstanceName::originalName() const
{
return m_original_name;
}
QString InstanceName::modifiedName() const
{
if (!m_modified_name.isEmpty())
return m_modified_name;
return m_original_name;
}
QString InstanceName::version() const
{
return m_original_version;
}
void InstanceName::setName(InstanceName& other)
{
m_original_name = other.m_original_name;
m_original_version = other.m_original_version;
m_modified_name = other.m_modified_name;
}
InstanceTask::InstanceTask() : Task(), InstanceName() {}

View File

@ -1,52 +1,57 @@
#pragma once #pragma once
#include "tasks/Task.h"
#include "settings/SettingsObject.h" #include "settings/SettingsObject.h"
#include "tasks/Task.h"
class InstanceTask : public Task /* Helpers */
{ enum class InstanceNameChange { ShouldChange, ShouldKeep };
[[nodiscard]] InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name);
struct InstanceName {
public:
InstanceName() = default;
InstanceName(QString name, QString version) : m_original_name(std::move(name)), m_original_version(std::move(version)) {}
[[nodiscard]] QString modifiedName() const;
[[nodiscard]] QString originalName() const;
[[nodiscard]] QString name() const;
[[nodiscard]] QString version() const;
void setName(QString name) { m_modified_name = name; }
void setName(InstanceName& other);
protected:
QString m_original_name;
QString m_original_version;
QString m_modified_name;
};
class InstanceTask : public Task, public InstanceName {
Q_OBJECT Q_OBJECT
public: public:
explicit InstanceTask(); InstanceTask();
virtual ~InstanceTask(); ~InstanceTask() override = default;
void setParentSettings(SettingsObjectPtr settings) void setParentSettings(SettingsObjectPtr settings) { m_globalSettings = settings; }
{
m_globalSettings = settings;
}
void setStagingPath(const QString &stagingPath) void setStagingPath(const QString& stagingPath) { m_stagingPath = stagingPath; }
{
m_stagingPath = stagingPath;
}
void setName(const QString &name) void setIcon(const QString& icon) { m_instIcon = icon; }
{
m_instName = name;
}
QString name() const
{
return m_instName;
}
void setIcon(const QString &icon) void setGroup(const QString& group) { m_instGroup = group; }
{ QString group() const { return m_instGroup; }
m_instIcon = icon;
}
void setGroup(const QString &group) bool shouldOverride() const { return m_override_existing; }
{
m_instGroup = group; protected:
} void setOverride(bool override) { m_override_existing = override; }
QString group() const
{
return m_instGroup;
}
protected: /* data */ protected: /* data */
SettingsObjectPtr m_globalSettings; SettingsObjectPtr m_globalSettings;
QString m_instName;
QString m_instIcon; QString m_instIcon;
QString m_instGroup; QString m_instGroup;
QString m_stagingPath; QString m_stagingPath;
bool m_override_existing = false;
}; };

View File

@ -93,8 +93,8 @@ void LaunchController::decideAccount()
auto reply = CustomMessageBox::selectable( auto reply = CustomMessageBox::selectable(
m_parentWidget, m_parentWidget,
tr("No Accounts"), tr("No Accounts"),
tr("In order to play Minecraft, you must have at least one Mojang or Microsoft " tr("In order to play Minecraft, you must have at least one Microsoft or Mojang "
"account logged in. " "account logged in. Mojang accounts can only be used offline. "
"Would you like to open the account manager to add an account now?"), "Would you like to open the account manager to add an account now?"),
QMessageBox::Information, QMessageBox::Information,
QMessageBox::Yes | QMessageBox::No QMessageBox::Yes | QMessageBox::No
@ -145,18 +145,29 @@ void LaunchController::login() {
return; return;
} }
// we try empty password first :)
QString password;
// we loop until the user succeeds in logging in or gives up // we loop until the user succeeds in logging in or gives up
bool tryagain = true; bool tryagain = true;
// the failure. the default failure. unsigned int tries = 0;
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;
while (tryagain) 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 = std::make_shared<AuthSession>();
m_session->wants_online = m_online; m_session->wants_online = m_online;
m_session->demo = m_demo;
m_accountToUse->fillSession(m_session); m_accountToUse->fillSession(m_session);
// Launch immediately in true offline mode // Launch immediately in true offline mode
@ -174,12 +185,18 @@ void LaunchController::login() {
if(!m_session->wants_online) { if(!m_session->wants_online) {
// we ask the user for a player name // we ask the user for a player name
bool ok = false; bool ok = false;
QString message = tr("Choose your offline mode player name.");
if(m_session->demo) {
message = tr("Choose your demo mode player name.");
}
QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString();
QString usedname = lastOfflinePlayerName.isEmpty() ? m_session->player_name : lastOfflinePlayerName; QString usedname = lastOfflinePlayerName.isEmpty() ? m_session->player_name : lastOfflinePlayerName;
QString name = QInputDialog::getText( QString name = QInputDialog::getText(
m_parentWidget, m_parentWidget,
tr("Player name"), tr("Player name"),
tr("Choose your offline mode player name."), message,
QLineEdit::Normal, QLineEdit::Normal,
usedname, usedname,
&ok &ok
@ -359,7 +376,7 @@ void LaunchController::launchInstance()
} }
m_launcher->prependStep(new TextPrint(m_launcher.get(), resolved_servers, MessageLevel::Launcher)); m_launcher->prependStep(new TextPrint(m_launcher.get(), resolved_servers, MessageLevel::Launcher));
} else { } else {
online_mode = "offline"; online_mode = m_demo ? "demo" : "offline";
} }
m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher));

View File

@ -63,6 +63,10 @@ public:
m_online = online; m_online = online;
} }
void setDemo(bool demo) {
m_demo = demo;
}
void setProfiler(BaseProfilerFactory *profiler) { void setProfiler(BaseProfilerFactory *profiler) {
m_profiler = profiler; m_profiler = profiler;
} }
@ -101,6 +105,7 @@ private slots:
private: private:
BaseProfilerFactory *m_profiler = nullptr; BaseProfilerFactory *m_profiler = nullptr;
bool m_online = true; bool m_online = true;
bool m_demo = false;
InstancePtr m_instance; InstancePtr m_instance;
QWidget * m_parentWidget = nullptr; QWidget * m_parentWidget = nullptr;
InstanceWindow *m_console = nullptr; InstanceWindow *m_console = nullptr;

View File

@ -34,8 +34,9 @@
*/ */
#include "LoggedProcess.h" #include "LoggedProcess.h"
#include "MessageLevel.h"
#include <QDebug> #include <QDebug>
#include <QTextDecoder>
#include "MessageLevel.h"
LoggedProcess::LoggedProcess(QObject *parent) : QProcess(parent) 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); auto str = decoder.toUnicode(data);
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
str.remove('\r'); auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed, QString::SkipEmptyParts);
QStringList lines = str.split("\n"); #else
leftover = lines.takeLast(); auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed, Qt::SkipEmptyParts);
#endif
return lines; return lines;
} }
void LoggedProcess::on_stdErr() void LoggedProcess::on_stdErr()
{ {
auto lines = reprocess(readAllStandardError(), m_err_leftover); auto lines = reprocess(readAllStandardError(), m_err_decoder);
emit log(lines, MessageLevel::StdErr); emit log(lines, MessageLevel::StdErr);
} }
void LoggedProcess::on_stdOut() void LoggedProcess::on_stdOut()
{ {
auto lines = reprocess(readAllStandardOutput(), m_out_leftover); auto lines = reprocess(readAllStandardOutput(), m_out_decoder);
emit log(lines, MessageLevel::StdOut); emit log(lines, MessageLevel::StdOut);
} }
@ -86,18 +88,6 @@ void LoggedProcess::on_exit(int exit_code, QProcess::ExitStatus status)
// save the exit code // save the exit code
m_exit_code = 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 // based on state, send signals
if (!m_is_aborting) if (!m_is_aborting)
{ {

View File

@ -36,6 +36,7 @@
#pragma once #pragma once
#include <QProcess> #include <QProcess>
#include <QTextDecoder>
#include "MessageLevel.h" #include "MessageLevel.h"
/* /*
@ -88,8 +89,8 @@ private:
void changeState(LoggedProcess::State state); void changeState(LoggedProcess::State state);
private: private:
QString m_err_leftover; QTextDecoder m_err_decoder = QTextDecoder(QTextCodec::codecForLocale());
QString m_out_leftover; QTextDecoder m_out_decoder = QTextDecoder(QTextCodec::codecForLocale());
bool m_killed = false; bool m_killed = false;
State m_state = NotRunning; State m_state = NotRunning;
int m_exit_code = 0; int m_exit_code = 0;

View File

@ -141,13 +141,14 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
QSet<QString> addedFiles; QSet<QString> addedFiles;
// Modify the jar // Modify the jar
for (auto i = mods.constEnd(); i != mods.constBegin(); --i) // This needs to be done in reverse-order to ensure we respect the loading order of components
for (auto i = mods.crbegin(); i != mods.crend(); i++)
{ {
const Mod* mod = *i; const auto* mod = *i;
// do not merge disabled mods. // do not merge disabled mods.
if (!mod->enabled()) if (!mod->enabled())
continue; continue;
if (mod->type() == Mod::MOD_ZIPFILE) if (mod->type() == ResourceType::ZIPFILE)
{ {
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles))
{ {
@ -157,7 +158,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
return false; return false;
} }
} }
else if (mod->type() == Mod::MOD_SINGLEFILE) else if (mod->type() == ResourceType::SINGLEFILE)
{ {
// FIXME: buggy - does not work with addedFiles // FIXME: buggy - does not work with addedFiles
auto filename = mod->fileinfo(); auto filename = mod->fileinfo();
@ -170,7 +171,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
} }
addedFiles.insert(filename.fileName()); 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 // untested, but seems to be unused / not possible to reach
// FIXME: buggy - does not work with addedFiles // FIXME: buggy - does not work with addedFiles
@ -267,7 +268,7 @@ bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & re
// ours // 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); QDir directory(target);
QStringList extracted; QStringList extracted;
@ -276,7 +277,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString &
auto numEntries = zip->getEntriesCount(); auto numEntries = zip->getEntriesCount();
if(numEntries < 0) { if(numEntries < 0) {
qWarning() << "Failed to enumerate files in archive"; qWarning() << "Failed to enumerate files in archive";
return nonstd::nullopt; return std::nullopt;
} }
else if(numEntries == 0) { else if(numEntries == 0) {
qDebug() << "Extracting empty archives seems odd..."; qDebug() << "Extracting empty archives seems odd...";
@ -285,7 +286,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString &
else if (!zip->goToFirstFile()) else if (!zip->goToFirstFile())
{ {
qWarning() << "Failed to seek to first file in zip"; qWarning() << "Failed to seek to first file in zip";
return nonstd::nullopt; return std::nullopt;
} }
do do
@ -322,7 +323,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString &
{ {
qWarning() << "Failed to extract file" << original_name << "to" << absFilePath; qWarning() << "Failed to extract file" << original_name << "to" << absFilePath;
JlCompress::removeFile(extracted); JlCompress::removeFile(extracted);
return nonstd::nullopt; return std::nullopt;
} }
extracted.append(absFilePath); extracted.append(absFilePath);
@ -340,7 +341,7 @@ bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &tar
} }
// ours // ours
nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir) std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir)
{ {
QuaZip zip(fileCompressed); QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip)) if (!zip.open(QuaZip::mdUnzip))
@ -351,13 +352,13 @@ nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString
return QStringList(); return QStringList();
} }
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
return nonstd::nullopt; return std::nullopt;
} }
return MMCZip::extractSubDir(&zip, "", dir); return MMCZip::extractSubDir(&zip, "", dir);
} }
// ours // 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); QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip)) if (!zip.open(QuaZip::mdUnzip))
@ -368,7 +369,7 @@ nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString
return QStringList(); return QStringList();
} }
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
return nonstd::nullopt; return std::nullopt;
} }
return MMCZip::extractSubDir(&zip, subdir, dir); return MMCZip::extractSubDir(&zip, subdir, dir);
} }

View File

@ -42,7 +42,7 @@
#include <functional> #include <functional>
#include <quazip/JlCompress.h> #include <quazip/JlCompress.h>
#include <nonstd/optional> #include <optional>
namespace MMCZip namespace MMCZip
{ {
@ -95,7 +95,7 @@ namespace MMCZip
/** /**
* Extract a subdirectory from an archive * 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); 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. * \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. * \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 * 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. * \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. * \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 * Extract a single file from an archive into a directory

View File

@ -1,3 +1,38 @@
// 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.
*/
#pragma once #pragma once
#include "BaseInstance.h" #include "BaseInstance.h"
#include "launch/LaunchTask.h" #include "launch/LaunchTask.h"
@ -15,6 +50,10 @@ public:
void saveNow() override void saveNow() override
{ {
} }
void loadSpecificSettings() override
{
setSpecificSettingsLoaded(true);
}
QString getStatusbarDescription() override QString getStatusbarDescription() override
{ {
return tr("Unknown instance type"); return tr("Unknown instance type");
@ -43,7 +82,7 @@ public:
{ {
return QProcessEnvironment(); return QProcessEnvironment();
} }
QMap<QString, QString> getVariables() const override QMap<QString, QString> getVariables() override
{ {
return QMap<QString, QString>(); return QMap<QString, QString>();
} }
@ -80,4 +119,8 @@ public:
QString modsRoot() const override { QString modsRoot() const override {
return QString(); return QString();
} }
void updateRuntimeContext()
{
// NOOP
}
}; };

View File

@ -1,89 +1,37 @@
#pragma once #pragma once
#include <QObject>
#include <QSharedPointer>
#include <functional> #include <functional>
#include <memory> #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 * A unique pointer class with unique pointer semantics intended for derivates of QObject
* Calls deleteLater() instead of destroying the contained object immediately * 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 * A shared pointer class with shared pointer semantics intended for derivates of QObject
* Calls deleteLater() instead of destroying the contained object immediately * Calls deleteLater() instead of destroying the contained object immediately
*/ */
template <typename T> template <typename T>
class shared_qobject_ptr class shared_qobject_ptr : public QSharedPointer<T> {
{
public: public:
shared_qobject_ptr(){} constexpr shared_qobject_ptr() : QSharedPointer<T>() {}
shared_qobject_ptr(T * wrap) 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) {}
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();
}
public: template <typename Derived>
void reset(T * wrap) constexpr shared_qobject_ptr(const shared_qobject_ptr<Derived>& other) : QSharedPointer<T>(other)
{ {}
using namespace std::placeholders;
m_ptr.reset(wrap, std::bind(&QObject::deleteLater, _1)); void reset() { QSharedPointer<T>::reset(); }
}
void reset(const shared_qobject_ptr<T>& other) void reset(const shared_qobject_ptr<T>& other)
{ {
m_ptr = other.m_ptr; shared_qobject_ptr<T> t(other);
this->swap(t);
} }
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
{
return m_ptr;
}
bool operator==(const shared_qobject_ptr<T>& other) {
return m_ptr == other.m_ptr;
}
bool operator!=(const shared_qobject_ptr<T>& other) {
return m_ptr != other.m_ptr;
}
private:
std::shared_ptr <T> m_ptr;
}; };

80
launcher/RuntimeContext.h Normal file
View File

@ -0,0 +1,80 @@
// 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/>.
*/
#pragma once
#include <QSet>
#include <QString>
#include "settings/SettingsObject.h"
struct RuntimeContext {
QString javaArchitecture;
QString javaRealArchitecture;
QString javaPath;
QString system;
QString mappedJavaRealArchitecture() const {
if (javaRealArchitecture == "aarch64") {
return "arm64";
}
return javaRealArchitecture;
}
void updateFromInstanceSettings(SettingsObjectPtr instanceSettings) {
javaArchitecture = instanceSettings->get("JavaArchitecture").toString();
javaRealArchitecture = instanceSettings->get("JavaRealArchitecture").toString();
javaPath = instanceSettings->get("JavaPath").toString();
system = currentSystem();
}
QString getClassifier() const {
return system + "-" + mappedJavaRealArchitecture();
}
// "Legacy" refers to the fact that Mojang assumed that these are the only two architectures
bool isLegacyArch() const {
QSet<QString> legacyArchitectures{"amd64", "x86_64", "i386", "i686", "x86"};
return legacyArchitectures.contains(mappedJavaRealArchitecture());
}
bool classifierMatches(QString target) const {
// try to match precise classifier "[os]-[arch]"
bool x = target == getClassifier();
// try to match imprecise classifier on legacy architectures "[os]"
if (!x && isLegacyArch())
x = target == system;
return x;
}
static QString currentSystem() {
#if defined(Q_OS_LINUX)
return "linux";
#elif defined(Q_OS_MACOS)
return "osx";
#elif defined(Q_OS_WINDOWS)
return "windows";
#elif defined(Q_OS_FREEBSD)
return "freebsd";
#elif defined(Q_OS_OPENBSD)
return "openbsd";
#else
return "unknown";
#endif
}
};

View File

@ -121,6 +121,7 @@ QProcessEnvironment CleanEnviroment()
qDebug() << "Env: stripped" << key << value << "to" << newValue; qDebug() << "Env: stripped" << key << value << "to" << newValue;
} }
#endif
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
// Strip IBus // Strip IBus
// IBus is a Linux IME framework. For some reason, it breaks MC? // IBus is a Linux IME framework. For some reason, it breaks MC?
@ -173,11 +174,17 @@ JavaInstallPtr JavaUtils::GetDefaultJava()
QStringList addJavasFromEnv(QList<QString> javas) QStringList addJavasFromEnv(QList<QString> javas)
{ {
QByteArray env = qgetenv("SNEEDMC_JAVA_PATHS"); QString env = qgetenv("SNEEDMC_JAVA_PATHS");
#if defined(Q_OS_WIN32) #if defined(Q_OS_WIN32)
QList<QString> javaPaths = QString::fromLocal8Bit(env).replace("\\", "/").split(QLatin1String(";")); QList<QString> javaPaths = env.replace("\\", "/").split(';');
auto envPath = qEnvironmentVariable("PATH");
QList<QString> javaPathsfromPath = envPath.replace("\\", "/").split(';');
for (QString string : javaPathsfromPath) {
javaPaths.append(string + "/javaw.exe");
}
#else #else
QList<QString> javaPaths = QString::fromLocal8Bit(env).split(QLatin1String(":")); QList<QString> javaPaths = env.split(':');
#endif #endif
for(QString i : javaPaths) for(QString i : javaPaths)
{ {

View File

@ -121,7 +121,6 @@ void CheckJava::checkJavaFinished(JavaCheckResult result)
emit logLine(QString("Could not start java:"), MessageLevel::Error); emit logLine(QString("Could not start java:"), MessageLevel::Error);
emit logLines(result.errorLog.split('\n'), MessageLevel::Error); emit logLines(result.errorLog.split('\n'), MessageLevel::Error);
emit logLine(QString("\nCheck your SneedMC Java settings."), MessageLevel::Launcher); emit logLine(QString("\nCheck your SneedMC Java settings."), MessageLevel::Launcher);
printSystemInfo(false, false);
emitFailed(QString("Could not start java!")); emitFailed(QString("Could not start java!"));
return; return;
} }
@ -130,7 +129,6 @@ void CheckJava::checkJavaFinished(JavaCheckResult result)
emit logLine(QString("Java checker returned some invalid data SneedMC doesn't understand:"), MessageLevel::Error); emit logLine(QString("Java checker returned some invalid data SneedMC doesn't understand:"), MessageLevel::Error);
emit logLines(result.outLog.split('\n'), MessageLevel::Warning); emit logLines(result.outLog.split('\n'), MessageLevel::Warning);
emit logLine("\nMinecraft might not start properly.", MessageLevel::Launcher); emit logLine("\nMinecraft might not start properly.", MessageLevel::Launcher);
printSystemInfo(false, false);
emitSucceeded(); emitSucceeded();
return; return;
} }
@ -138,7 +136,6 @@ void CheckJava::checkJavaFinished(JavaCheckResult result)
{ {
auto instance = m_parent->instance(); auto instance = m_parent->instance();
printJavaInfo(result.javaVersion.toString(), result.mojangPlatform, result.realPlatform, result.javaVendor); printJavaInfo(result.javaVersion.toString(), result.mojangPlatform, result.realPlatform, result.javaVendor);
printSystemInfo(true, result.is_64bit);
instance->settings()->set("JavaVersion", result.javaVersion.toString()); instance->settings()->set("JavaVersion", result.javaVersion.toString());
instance->settings()->set("JavaArchitecture", result.mojangPlatform); instance->settings()->set("JavaArchitecture", result.mojangPlatform);
instance->settings()->set("JavaRealArchitecture", result.realPlatform); instance->settings()->set("JavaRealArchitecture", result.realPlatform);
@ -155,20 +152,3 @@ void CheckJava::printJavaInfo(const QString& version, const QString& architectur
emit logLine(QString("Java is version %1, using %2 (%3) architecture, from %4.\n\n") emit logLine(QString("Java is version %1, using %2 (%3) architecture, from %4.\n\n")
.arg(version, architecture, realArchitecture, vendor), MessageLevel::Launcher); .arg(version, architecture, realArchitecture, vendor), MessageLevel::Launcher);
} }
void CheckJava::printSystemInfo(bool javaIsKnown, bool javaIs64bit)
{
auto cpu64 = Sys::isCPU64bit();
auto system64 = Sys::isSystem64bit();
if(cpu64 != system64)
{
emit logLine(QString("Your CPU architecture is not matching your system architecture. You might want to install a 64bit Operating System.\n\n"), MessageLevel::Error);
}
if(javaIsKnown)
{
if(javaIs64bit != system64)
{
emit logLine(QString("Your Java architecture is not matching your system architecture. You might want to install a 64bit Java version.\n\n"), MessageLevel::Error);
}
}
}

View File

@ -36,10 +36,10 @@ private slots:
private: private:
void printJavaInfo(const QString & version, const QString & architecture, const QString & realArchitecture, const QString & vendor); void printJavaInfo(const QString & version, const QString & architecture, const QString & realArchitecture, const QString & vendor);
void printSystemInfo(bool javaIsKnown, bool javaIs64bit);
private: private:
QString m_javaPath; QString m_javaPath;
qlonglong m_javaUnixTime; qlonglong m_javaUnixTime;
JavaCheckerPtr m_JavaChecker; JavaCheckerPtr m_JavaChecker;
}; };

View File

@ -140,6 +140,13 @@ VersionPtr VersionList::getVersion(const QString &version)
return out; 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) void VersionList::setName(const QString &name)
{ {
m_name = name; m_name = name;

View File

@ -66,6 +66,7 @@ public:
QString humanReadable() const; QString humanReadable() const;
VersionPtr getVersion(const QString &version); VersionPtr getVersion(const QString &version);
bool hasVersion(QString version) const;
QVector<VersionPtr> versions() const QVector<VersionPtr> versions() const
{ {

View File

@ -1,3 +1,38 @@
// 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 <meta/VersionList.h> #include <meta/VersionList.h>
#include <meta/Index.h> #include <meta/Index.h>
#include "Component.h" #include "Component.h"
@ -60,7 +95,7 @@ void Component::applyTo(LaunchProfile* profile)
auto vfile = getVersionFile(); auto vfile = getVersionFile();
if(vfile) if(vfile)
{ {
vfile->applyTo(profile); vfile->applyTo(profile, m_parent->runtimeContext());
} }
else else
{ {

View File

@ -173,9 +173,9 @@ void LaunchProfile::applyCompatibleJavaMajors(QList<int>& javaMajor)
m_compatibleJavaMajors.append(javaMajor); m_compatibleJavaMajors.append(javaMajor);
} }
void LaunchProfile::applyLibrary(LibraryPtr library) void LaunchProfile::applyLibrary(LibraryPtr library, const RuntimeContext & runtimeContext)
{ {
if(!library->isActive()) if(!library->isActive(runtimeContext))
{ {
return; return;
} }
@ -205,9 +205,9 @@ void LaunchProfile::applyLibrary(LibraryPtr library)
} }
} }
void LaunchProfile::applyMavenFile(LibraryPtr mavenFile) void LaunchProfile::applyMavenFile(LibraryPtr mavenFile, const RuntimeContext & runtimeContext)
{ {
if(!mavenFile->isActive()) if(!mavenFile->isActive(runtimeContext))
{ {
return; return;
} }
@ -221,10 +221,10 @@ void LaunchProfile::applyMavenFile(LibraryPtr mavenFile)
m_mavenFiles.append(Library::limitedCopy(mavenFile)); m_mavenFiles.append(Library::limitedCopy(mavenFile));
} }
void LaunchProfile::applyAgent(AgentPtr agent) void LaunchProfile::applyAgent(AgentPtr agent, const RuntimeContext & runtimeContext)
{ {
auto lib = agent->library(); auto lib = agent->library();
if(!lib->isActive()) if(!lib->isActive(runtimeContext))
{ {
return; return;
} }
@ -354,7 +354,7 @@ const QList<int> & LaunchProfile::getCompatibleJavaMajors() const
} }
void LaunchProfile::getLibraryFiles( void LaunchProfile::getLibraryFiles(
const QString& architecture, const RuntimeContext & runtimeContext,
QStringList& jars, QStringList& jars,
QStringList& nativeJars, QStringList& nativeJars,
const QString& overridePath, const QString& overridePath,
@ -366,7 +366,7 @@ void LaunchProfile::getLibraryFiles(
nativeJars.clear(); nativeJars.clear();
for (auto lib : getLibraries()) for (auto lib : getLibraries())
{ {
lib->getApplicableFiles(currentSystem, jars, nativeJars, native32, native64, overridePath); lib->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath);
} }
// NOTE: order is important here, add main jar last to the lists // NOTE: order is important here, add main jar last to the lists
if(m_mainJar) if(m_mainJar)
@ -379,18 +379,18 @@ void LaunchProfile::getLibraryFiles(
} }
else else
{ {
m_mainJar->getApplicableFiles(currentSystem, jars, nativeJars, native32, native64, overridePath); m_mainJar->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath);
} }
} }
for (auto lib : getNativeLibraries()) for (auto lib : getNativeLibraries())
{ {
lib->getApplicableFiles(currentSystem, jars, nativeJars, native32, native64, overridePath); lib->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath);
} }
if(architecture == "32") if(runtimeContext.javaArchitecture == "32")
{ {
nativeJars.append(native32); nativeJars.append(native32);
} }
else if(architecture == "64") else if(runtimeContext.javaArchitecture == "64")
{ {
nativeJars.append(native64); nativeJars.append(native64);
} }

View File

@ -56,9 +56,9 @@ public: /* application of profile variables from patches */
void applyTweakers(const QStringList &tweakers); void applyTweakers(const QStringList &tweakers);
void applyJarMods(const QList<LibraryPtr> &jarMods); void applyJarMods(const QList<LibraryPtr> &jarMods);
void applyMods(const QList<LibraryPtr> &jarMods); void applyMods(const QList<LibraryPtr> &jarMods);
void applyLibrary(LibraryPtr library); void applyLibrary(LibraryPtr library, const RuntimeContext & runtimeContext);
void applyMavenFile(LibraryPtr library); void applyMavenFile(LibraryPtr library, const RuntimeContext & runtimeContext);
void applyAgent(AgentPtr agent); void applyAgent(AgentPtr agent, const RuntimeContext & runtimeContext);
void applyCompatibleJavaMajors(QList<int>& javaMajor); void applyCompatibleJavaMajors(QList<int>& javaMajor);
void applyMainJar(LibraryPtr jar); void applyMainJar(LibraryPtr jar);
void applyProblemSeverity(ProblemSeverity severity); void applyProblemSeverity(ProblemSeverity severity);
@ -83,7 +83,7 @@ public: /* getters for profile variables */
const QList<int> & getCompatibleJavaMajors() const; const QList<int> & getCompatibleJavaMajors() const;
const LibraryPtr getMainJar() const; const LibraryPtr getMainJar() const;
void getLibraryFiles( void getLibraryFiles(
const QString & architecture, const RuntimeContext & runtimeContext,
QStringList & jars, QStringList & jars,
QStringList & nativeJars, QStringList & nativeJars,
const QString & overridePath, const QString & overridePath,

View File

@ -1,3 +1,38 @@
// 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 "Library.h" #include "Library.h"
#include "MinecraftInstance.h" #include "MinecraftInstance.h"
@ -7,7 +42,7 @@
#include <BuildConfig.h> #include <BuildConfig.h>
void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& native, QStringList& native32, void Library::getApplicableFiles(const RuntimeContext & runtimeContext, QStringList& jar, QStringList& native, QStringList& native32,
QStringList& native64, const QString &overridePath) const QStringList& native64, const QString &overridePath) const
{ {
bool local = isLocal(); bool local = isLocal();
@ -21,7 +56,7 @@ void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& na
} }
return out.absoluteFilePath(); return out.absoluteFilePath();
}; };
QString raw_storage = storageSuffix(system); QString raw_storage = storageSuffix(runtimeContext);
if(isNative()) if(isNative())
{ {
if (raw_storage.contains("${arch}")) if (raw_storage.contains("${arch}"))
@ -45,7 +80,7 @@ void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& na
} }
QList<NetAction::Ptr> Library::getDownloads( QList<NetAction::Ptr> Library::getDownloads(
OpSys system, const RuntimeContext & runtimeContext,
class HttpMetaCache* cache, class HttpMetaCache* cache,
QStringList& failedLocalFiles, QStringList& failedLocalFiles,
const QString & overridePath const QString & overridePath
@ -88,6 +123,9 @@ QList<NetAction::Ptr> Library::getDownloads(
options |= Net::Download::Option::AcceptLocalFiles; 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()) if(sha1.size())
{ {
auto rawSha1 = QByteArray::fromHex(sha1.toLatin1()); auto rawSha1 = QByteArray::fromHex(sha1.toLatin1());
@ -104,14 +142,14 @@ QList<NetAction::Ptr> Library::getDownloads(
return true; return true;
}; };
QString raw_storage = storageSuffix(system); QString raw_storage = storageSuffix(runtimeContext);
if(m_mojangDownloads) if(m_mojangDownloads)
{ {
if(isNative()) if(isNative())
{ {
if(m_nativeClassifiers.contains(system)) auto nativeClassifier = getCompatibleNative(runtimeContext);
if(!nativeClassifier.isNull())
{ {
auto nativeClassifier = m_nativeClassifiers[system];
if(nativeClassifier.contains("${arch}")) if(nativeClassifier.contains("${arch}"))
{ {
auto nat32Classifier = nativeClassifier; auto nat32Classifier = nativeClassifier;
@ -200,7 +238,7 @@ QList<NetAction::Ptr> Library::getDownloads(
return out; return out;
} }
bool Library::isActive() const bool Library::isActive(const RuntimeContext & runtimeContext) const
{ {
bool result = true; bool result = true;
if (m_rules.empty()) if (m_rules.empty())
@ -212,7 +250,7 @@ bool Library::isActive() const
RuleAction ruleResult = Disallow; RuleAction ruleResult = Disallow;
for (auto rule : m_rules) for (auto rule : m_rules)
{ {
RuleAction temp = rule->apply(this); RuleAction temp = rule->apply(this, runtimeContext);
if (temp != Defer) if (temp != Defer)
ruleResult = temp; ruleResult = temp;
} }
@ -220,7 +258,7 @@ bool Library::isActive() const
} }
if (isNative()) if (isNative())
{ {
result = result && m_nativeClassifiers.contains(currentSystem); result = result && !getCompatibleNative(runtimeContext).isNull();
} }
return result; return result;
} }
@ -235,6 +273,19 @@ bool Library::isAlwaysStale() const
return m_hint == "always-stale"; return m_hint == "always-stale";
} }
QString Library::getCompatibleNative(const RuntimeContext & runtimeContext) const {
// try to match precise classifier "[os]-[arch]"
auto entry = m_nativeClassifiers.constFind(runtimeContext.getClassifier());
// try to match imprecise classifier on legacy architectures "[os]"
if (entry == m_nativeClassifiers.constEnd() && runtimeContext.isLegacyArch())
entry = m_nativeClassifiers.constFind(runtimeContext.system);
if (entry == m_nativeClassifiers.constEnd())
return QString();
return entry.value();
}
void Library::setStoragePrefix(QString prefix) void Library::setStoragePrefix(QString prefix)
{ {
m_storagePrefix = prefix; m_storagePrefix = prefix;
@ -254,7 +305,7 @@ QString Library::storagePrefix() const
return m_storagePrefix; return m_storagePrefix;
} }
QString Library::filename(OpSys system) const QString Library::filename(const RuntimeContext & runtimeContext) const
{ {
if(!m_filename.isEmpty()) if(!m_filename.isEmpty())
{ {
@ -268,9 +319,10 @@ QString Library::filename(OpSys system) const
// otherwise native, override classifiers. Mojang HACK! // otherwise native, override classifiers. Mojang HACK!
GradleSpecifier nativeSpec = m_name; GradleSpecifier nativeSpec = m_name;
if (m_nativeClassifiers.contains(system)) QString nativeClassifier = getCompatibleNative(runtimeContext);
if (!nativeClassifier.isNull())
{ {
nativeSpec.setClassifier(m_nativeClassifiers[system]); nativeSpec.setClassifier(nativeClassifier);
} }
else else
{ {
@ -279,14 +331,14 @@ QString Library::filename(OpSys system) const
return nativeSpec.getFileName(); return nativeSpec.getFileName();
} }
QString Library::displayName(OpSys system) const QString Library::displayName(const RuntimeContext & runtimeContext) const
{ {
if(!m_displayname.isEmpty()) if(!m_displayname.isEmpty())
return m_displayname; return m_displayname;
return filename(system); return filename(runtimeContext);
} }
QString Library::storageSuffix(OpSys system) const QString Library::storageSuffix(const RuntimeContext & runtimeContext) const
{ {
// non-native? use only the gradle specifier // non-native? use only the gradle specifier
if (!isNative()) if (!isNative())
@ -296,9 +348,10 @@ QString Library::storageSuffix(OpSys system) const
// otherwise native, override classifiers. Mojang HACK! // otherwise native, override classifiers. Mojang HACK!
GradleSpecifier nativeSpec = m_name; GradleSpecifier nativeSpec = m_name;
if (m_nativeClassifiers.contains(system)) QString nativeClassifier = getCompatibleNative(runtimeContext);
if (!nativeClassifier.isNull())
{ {
nativeSpec.setClassifier(m_nativeClassifiers[system]); nativeSpec.setClassifier(nativeClassifier);
} }
else else
{ {

View File

@ -1,8 +1,44 @@
// 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.
*/
#pragma once #pragma once
#include <QString> #include <QString>
#include <net/NetAction.h> #include <net/NetAction.h>
#include <QPair> #include <QPair>
#include <QList> #include <QList>
#include <QString>
#include <QStringList> #include <QStringList>
#include <QMap> #include <QMap>
#include <QDir> #include <QDir>
@ -10,9 +46,9 @@
#include <memory> #include <memory>
#include "Rule.h" #include "Rule.h"
#include "minecraft/OpSys.h"
#include "GradleSpecifier.h" #include "GradleSpecifier.h"
#include "MojangDownloadInfo.h" #include "MojangDownloadInfo.h"
#include "RuntimeContext.h"
class Library; class Library;
class MinecraftInstance; class MinecraftInstance;
@ -98,7 +134,7 @@ public: /* methods */
m_repositoryURL = base_url; m_repositoryURL = base_url;
} }
void getApplicableFiles(OpSys system, QStringList & jar, QStringList & native, void getApplicableFiles(const RuntimeContext & runtimeContext, QStringList & jar, QStringList & native,
QStringList & native32, QStringList & native64, const QString & overridePath) const; QStringList & native32, QStringList & native64, const QString & overridePath) const;
void setAbsoluteUrl(const QString &absolute_url) void setAbsoluteUrl(const QString &absolute_url)
@ -112,7 +148,7 @@ public: /* methods */
} }
/// Get the file name of the library /// Get the file name of the library
QString filename(OpSys system) const; QString filename(const RuntimeContext & runtimeContext) const;
// DEPRECATED: set a display name, used by jar mods only // DEPRECATED: set a display name, used by jar mods only
void setDisplayName(const QString & displayName) void setDisplayName(const QString & displayName)
@ -121,7 +157,7 @@ public: /* methods */
} }
/// Get the file name of the library /// Get the file name of the library
QString displayName(OpSys system) const; QString displayName(const RuntimeContext & runtimeContext) const;
void setMojangDownloadInfo(MojangLibraryDownloadInfo::Ptr info) void setMojangDownloadInfo(MojangLibraryDownloadInfo::Ptr info)
{ {
@ -140,7 +176,7 @@ public: /* methods */
} }
/// Returns true if the library should be loaded (or extracted, in case of natives) /// Returns true if the library should be loaded (or extracted, in case of natives)
bool isActive() const; bool isActive(const RuntimeContext & runtimeContext) const;
/// Returns true if the library is contained in an instance and false if it is shared /// Returns true if the library is contained in an instance and false if it is shared
bool isLocal() const; bool isLocal() const;
@ -152,9 +188,11 @@ public: /* methods */
bool isForge() const; bool isForge() const;
// Get a list of downloads for this library // Get a list of downloads for this library
QList<NetAction::Ptr> getDownloads(OpSys system, class HttpMetaCache * cache, QList<NetAction::Ptr> getDownloads(const RuntimeContext & runtimeContext, class HttpMetaCache * cache,
QStringList & failedLocalFiles, const QString & overridePath) const; QStringList & failedLocalFiles, const QString & overridePath) const;
QString getCompatibleNative(const RuntimeContext & runtimeContext) const;
private: /* methods */ private: /* methods */
/// the default storage prefix used by SneedMC /// the default storage prefix used by SneedMC
static QString defaultStoragePrefix(); static QString defaultStoragePrefix();
@ -163,7 +201,7 @@ private: /* methods */
QString storagePrefix() const; QString storagePrefix() const;
/// Get the relative file path where the library should be saved /// Get the relative file path where the library should be saved
QString storageSuffix(OpSys system) const; QString storageSuffix(const RuntimeContext & runtimeContext) const;
QString hint() const QString hint() const
{ {
@ -204,7 +242,7 @@ protected: /* data */
QStringList m_extractExcludes; QStringList m_extractExcludes;
/// native suffixes per OS /// native suffixes per OS
QMap<OpSys, QString> m_nativeClassifiers; QMap<QString, QString> m_nativeClassifiers;
/// true if the library had a rules section (even empty) /// true if the library had a rules section (even empty)
bool applyRules = false; bool applyRules = false;

View File

@ -75,6 +75,7 @@
#include "mod/ModFolderModel.h" #include "mod/ModFolderModel.h"
#include "mod/ResourcePackFolderModel.h" #include "mod/ResourcePackFolderModel.h"
#include "mod/ShaderPackFolderModel.h"
#include "mod/TexturePackFolderModel.h" #include "mod/TexturePackFolderModel.h"
#include "WorldList.h" #include "WorldList.h"
@ -115,6 +116,19 @@ private:
MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir)
: BaseInstance(globalSettings, settings, 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 // Java Settings
auto javaOverride = m_settings->registerSetting("OverrideJava", false); auto javaOverride = m_settings->registerSetting("OverrideJava", false);
auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false); auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false);
@ -124,64 +138,66 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO
auto javaOrLocation = std::make_shared<OrSetting>("JavaOrLocationOverride", javaOverride, locationOverride); auto javaOrLocation = std::make_shared<OrSetting>("JavaOrLocationOverride", javaOverride, locationOverride);
auto javaOrArgs = std::make_shared<OrSetting>("JavaOrArgsOverride", javaOverride, argsOverride); auto javaOrArgs = std::make_shared<OrSetting>("JavaOrArgsOverride", javaOverride, argsOverride);
m_settings->registerOverride(globalSettings->getSetting("JavaPath"), javaOrLocation); if (auto global_settings = globalSettings()) {
m_settings->registerOverride(globalSettings->getSetting("JvmArgs"), javaOrArgs); m_settings->registerOverride(global_settings->getSetting("JavaPath"), javaOrLocation);
m_settings->registerOverride(globalSettings->getSetting("IgnoreJavaCompatibility"), javaOrLocation); m_settings->registerOverride(global_settings->getSetting("JvmArgs"), javaOrArgs);
m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), javaOrLocation);
// special! // special!
m_settings->registerPassthrough(globalSettings->getSetting("JavaTimestamp"), javaOrLocation); m_settings->registerPassthrough(global_settings->getSetting("JavaTimestamp"), javaOrLocation);
m_settings->registerPassthrough(globalSettings->getSetting("JavaVersion"), javaOrLocation); m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation);
m_settings->registerPassthrough(globalSettings->getSetting("JavaArchitecture"), javaOrLocation); m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), javaOrLocation);
m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), javaOrLocation);
// Window Size // Window Size
auto windowSetting = m_settings->registerSetting("OverrideWindow", false); auto windowSetting = m_settings->registerSetting("OverrideWindow", false);
m_settings->registerOverride(globalSettings->getSetting("LaunchMaximized"), windowSetting); m_settings->registerOverride(global_settings->getSetting("LaunchMaximized"), windowSetting);
m_settings->registerOverride(globalSettings->getSetting("MinecraftWinWidth"), windowSetting); m_settings->registerOverride(global_settings->getSetting("MinecraftWinWidth"), windowSetting);
m_settings->registerOverride(globalSettings->getSetting("MinecraftWinHeight"), windowSetting); m_settings->registerOverride(global_settings->getSetting("MinecraftWinHeight"), windowSetting);
// Memory // Memory
auto memorySetting = m_settings->registerSetting("OverrideMemory", false); auto memorySetting = m_settings->registerSetting("OverrideMemory", false);
m_settings->registerOverride(globalSettings->getSetting("MinMemAlloc"), memorySetting); m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting);
m_settings->registerOverride(globalSettings->getSetting("MaxMemAlloc"), memorySetting); m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting);
m_settings->registerOverride(globalSettings->getSetting("PermGen"), memorySetting); m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting);
// Minecraft launch method // Minecraft launch method
auto launchMethodOverride = m_settings->registerSetting("OverrideMCLaunchMethod", false); auto launchMethodOverride = m_settings->registerSetting("OverrideMCLaunchMethod", false);
m_settings->registerOverride(globalSettings->getSetting("MCLaunchMethod"), launchMethodOverride); m_settings->registerOverride(global_settings->getSetting("MCLaunchMethod"), launchMethodOverride);
// Native library workarounds // Native library workarounds
auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false); auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false);
m_settings->registerOverride(globalSettings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(global_settings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride);
m_settings->registerOverride(globalSettings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride);
// Peformance related options // Peformance related options
auto performanceOverride = m_settings->registerSetting("OverridePerformance", false); auto performanceOverride = m_settings->registerSetting("OverridePerformance", false);
m_settings->registerOverride(globalSettings->getSetting("EnableFeralGamemode"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride);
m_settings->registerOverride(globalSettings->getSetting("EnableMangoHud"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride);
m_settings->registerOverride(globalSettings->getSetting("UseDiscreteGpu"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride);
// Game time // Miscellaneous
auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false);
m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride); m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride);
m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"), gameTimeOverride); m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride);
m_settings->set("InstanceType", "OneSix");
}
// Join server on launch, this does not have a global override // Join server on launch, this does not have a global override
m_settings->registerSetting("JoinServerOnLaunch", false); m_settings->registerSetting("JoinServerOnLaunch", false);
m_settings->registerSetting("JoinServerOnLaunchAddress", ""); m_settings->registerSetting("JoinServerOnLaunchAddress", "");
// Miscellaneous qDebug() << "Instance-type specific settings were loaded!";
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"); setSpecificSettingsLoaded(true);
m_components.reset(new PackProfile(this)); updateRuntimeContext();
} }
void MinecraftInstance::saveNow() void MinecraftInstance::updateRuntimeContext()
{ {
m_components->saveNow(); m_runtimeContext.updateFromInstanceSettings(m_settings);
} }
QString MinecraftInstance::typeName() const QString MinecraftInstance::typeName() const
@ -237,6 +253,14 @@ QString MinecraftInstance::getLocalLibraryPath() const
return libraries_dir.absolutePath(); return libraries_dir.absolutePath();
} }
bool MinecraftInstance::supportsDemo() const
{
Version instance_ver { getPackProfile()->getComponentVersion("net.minecraft") };
// Demo mode was introduced in 1.3.1: https://minecraft.fandom.com/wiki/Demo_mode#History
// FIXME: Due to Version constraints atm, this can't handle well non-release versions
return instance_ver >= Version("1.3.1");
}
QString MinecraftInstance::jarModsDir() const QString MinecraftInstance::jarModsDir() const
{ {
QDir jarmods_dir(FS::PathCombine(instanceRoot(), "jarmods/")); QDir jarmods_dir(FS::PathCombine(instanceRoot(), "jarmods/"));
@ -308,12 +332,11 @@ QDir MinecraftInstance::versionsPath() const
return QDir::current().absoluteFilePath("versions"); return QDir::current().absoluteFilePath("versions");
} }
QStringList MinecraftInstance::getClassPath() const QStringList MinecraftInstance::getClassPath()
{ {
QStringList jars, nativeJars; QStringList jars, nativeJars;
auto javaArchitecture = settings()->get("JavaArchitecture").toString();
auto profile = m_components->getProfile(); auto profile = m_components->getProfile();
profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot()); profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot());
return jars; return jars;
} }
@ -323,16 +346,15 @@ QString MinecraftInstance::getMainClass() const
return profile->getMainClass(); return profile->getMainClass();
} }
QStringList MinecraftInstance::getNativeJars() const QStringList MinecraftInstance::getNativeJars()
{ {
QStringList jars, nativeJars; QStringList jars, nativeJars;
auto javaArchitecture = settings()->get("JavaArchitecture").toString();
auto profile = m_components->getProfile(); auto profile = m_components->getProfile();
profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot()); profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot());
return nativeJars; return nativeJars;
} }
QStringList MinecraftInstance::extraArguments() const QStringList MinecraftInstance::extraArguments()
{ {
auto list = BaseInstance::extraArguments(); auto list = BaseInstance::extraArguments();
auto version = getPackProfile(); auto version = getPackProfile();
@ -352,13 +374,13 @@ QStringList MinecraftInstance::extraArguments() const
for (auto agent : agents) for (auto agent : agents)
{ {
QStringList jar, temp1, temp2, temp3; QStringList jar, temp1, temp2, temp3;
agent->library()->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, getLocalLibraryPath()); agent->library()->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, getLocalLibraryPath());
list.append("-javaagent:"+jar[0]+(agent->argument().isEmpty() ? "" : "="+agent->argument())); list.append("-javaagent:"+jar[0]+(agent->argument().isEmpty() ? "" : "="+agent->argument()));
} }
return list; return list;
} }
QStringList MinecraftInstance::javaArguments() const QStringList MinecraftInstance::javaArguments()
{ {
QStringList args; QStringList args;
@ -415,7 +437,7 @@ QStringList MinecraftInstance::javaArguments() const
return args; return args;
} }
QMap<QString, QString> MinecraftInstance::getVariables() const QMap<QString, QString> MinecraftInstance::getVariables()
{ {
QMap<QString, QString> out; QMap<QString, QString> out;
out.insert("INST_NAME", name()); out.insert("INST_NAME", name());
@ -447,13 +469,11 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
QProcessEnvironment env = createEnvironment(); QProcessEnvironment env = createEnvironment();
#ifdef Q_OS_LINUX #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 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_PRELOAD", preload);
env.insert("LD_LIBRARY_PATH", lib_path);
env.insert("MANGOHUD", "1"); env.insert("MANGOHUD", "1");
} }
@ -612,8 +632,7 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS
// libraries and class path. // libraries and class path.
{ {
QStringList jars, nativeJars; QStringList jars, nativeJars;
auto javaArchitecture = settings()->get("JavaArchitecture").toString(); profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot());
profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot());
for(auto file: jars) for(auto file: jars)
{ {
launchScript += "cp " + file + "\n"; launchScript += "cp " + file + "\n";
@ -669,8 +688,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
{ {
out << "Libraries:"; out << "Libraries:";
QStringList jars, nativeJars; QStringList jars, nativeJars;
auto javaArchitecture = settings->get("JavaArchitecture").toString(); profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot());
profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot());
auto printLibFile = [&](const QString & path) auto printLibFile = [&](const QString & path)
{ {
QFileInfo info(path); QFileInfo info(path);
@ -701,14 +719,14 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
{ {
out << QString("%1:").arg(label); out << QString("%1:").arg(label);
auto modList = model.allMods(); auto modList = model.allMods();
std::sort(modList.begin(), modList.end(), [](Mod::Ptr a, Mod::Ptr b) { std::sort(modList.begin(), modList.end(), [](auto a, auto b) {
auto aName = a->fileinfo().completeBaseName(); auto aName = a->fileinfo().completeBaseName();
auto bName = b->fileinfo().completeBaseName(); auto bName = b->fileinfo().completeBaseName();
return aName.localeAwareCompare(bName) < 0; return aName.localeAwareCompare(bName) < 0;
}); });
for(auto mod: modList) for(auto mod: modList)
{ {
if(mod->type() == Mod::MOD_FOLDER) if(mod->type() == ResourceType::FOLDER)
{ {
out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)"; out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)";
continue; continue;
@ -735,8 +753,8 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
out << "Jar Mods:"; out << "Jar Mods:";
for(auto & jarmod: jarMods) for(auto & jarmod: jarMods)
{ {
auto displayname = jarmod->displayName(currentSystem); auto displayname = jarmod->displayName(runtimeContext());
auto realname = jarmod->filename(currentSystem); auto realname = jarmod->filename(runtimeContext());
if(displayname != realname) if(displayname != realname)
{ {
out << " " + displayname + " (" + realname + ")"; out << " " + displayname + " (" + realname + ")";
@ -898,6 +916,7 @@ QString MinecraftInstance::getStatusbarDescription()
Task::Ptr MinecraftInstance::createUpdateTask(Net::Mode mode) Task::Ptr MinecraftInstance::createUpdateTask(Net::Mode mode)
{ {
updateRuntimeContext();
switch (mode) switch (mode)
{ {
case Net::Mode::Offline: case Net::Mode::Offline:
@ -914,6 +933,7 @@ Task::Ptr MinecraftInstance::createUpdateTask(Net::Mode mode)
shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin)
{ {
updateRuntimeContext();
// FIXME: get rid of shared_from_this ... // FIXME: get rid of shared_from_this ...
auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(shared_from_this())); auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(shared_from_this()));
auto pptr = process.get(); auto pptr = process.get();
@ -944,9 +964,9 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(new CreateGameFolders(pptr)); 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))); serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(fullAddress)));
} }
@ -1054,10 +1074,10 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
QString MinecraftInstance::launchMethod() 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()); return JavaVersion(settings()->get("JavaVersion").toString());
} }
@ -1086,18 +1106,18 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const
return m_core_mod_list; return m_core_mod_list;
} }
std::shared_ptr<ModFolderModel> MinecraftInstance::resourcePackList() const std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList() const
{ {
if (!m_resource_pack_list) if (!m_resource_pack_list)
{ {
m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir())); m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir()));
m_resource_pack_list->disableInteraction(isRunning()); m_resource_pack_list->enableInteraction(!isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ModFolderModel::disableInteraction); connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ResourcePackFolderModel::disableInteraction);
} }
return m_resource_pack_list; return m_resource_pack_list;
} }
std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList() const
{ {
if (!m_texture_pack_list) if (!m_texture_pack_list)
{ {
@ -1108,11 +1128,11 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
return m_texture_pack_list; return m_texture_pack_list;
} }
std::shared_ptr<ModFolderModel> MinecraftInstance::shaderPackList() const std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList() const
{ {
if (!m_shader_pack_list) 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()); m_shader_pack_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_shader_pack_list.get(), &ModFolderModel::disableInteraction); connect(this, &BaseInstance::runningStatusChanged, m_shader_pack_list.get(), &ModFolderModel::disableInteraction);
} }
@ -1144,7 +1164,7 @@ QList<Mod*> MinecraftInstance::getJarMods() const
for (auto jarmod : profile->getJarMods()) for (auto jarmod : profile->getJarMods())
{ {
QStringList jar, temp1, temp2, temp3; QStringList jar, temp1, temp2, temp3;
jarmod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, jarmodsPath().absolutePath()); jarmod->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, jarmodsPath().absolutePath());
// QString filePath = jarmodsPath().absoluteFilePath(jarmod->filename(currentSystem)); // QString filePath = jarmodsPath().absoluteFilePath(jarmod->filename(currentSystem));
mods.push_back(new Mod(QFileInfo(jar[0]))); mods.push_back(new Mod(QFileInfo(jar[0])));
} }

View File

@ -1,3 +1,38 @@
// 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.
*/
#pragma once #pragma once
#include "BaseInstance.h" #include "BaseInstance.h"
#include <java/JavaVersion.h> #include <java/JavaVersion.h>
@ -7,6 +42,10 @@
#include "minecraft/launch/MinecraftServerTarget.h" #include "minecraft/launch/MinecraftServerTarget.h"
class ModFolderModel; class ModFolderModel;
class ResourceFolderModel;
class ResourcePackFolderModel;
class ShaderPackFolderModel;
class TexturePackFolderModel;
class WorldList; class WorldList;
class GameOptions; class GameOptions;
class LaunchStep; class LaunchStep;
@ -20,6 +59,8 @@ public:
virtual ~MinecraftInstance() {}; virtual ~MinecraftInstance() {};
virtual void saveNow() override; virtual void saveNow() override;
void loadSpecificSettings() override;
// FIXME: remove // FIXME: remove
QString typeName() const override; QString typeName() const override;
// FIXME: remove // FIXME: remove
@ -63,6 +104,10 @@ public:
// where the instance-local libraries should be // where the instance-local libraries should be
QString getLocalLibraryPath() const; QString getLocalLibraryPath() const;
/** Returns whether the instance, with its version, has support for demo mode. */
[[nodiscard]] bool supportsDemo() const;
void updateRuntimeContext();
////// Profile management ////// ////// Profile management //////
std::shared_ptr<PackProfile> getPackProfile() const; std::shared_ptr<PackProfile> getPackProfile() const;
@ -70,24 +115,24 @@ public:
////// Mod Lists ////// ////// Mod Lists //////
std::shared_ptr<ModFolderModel> loaderModList() const; std::shared_ptr<ModFolderModel> loaderModList() const;
std::shared_ptr<ModFolderModel> coreModList() const; std::shared_ptr<ModFolderModel> coreModList() const;
std::shared_ptr<ModFolderModel> resourcePackList() const; std::shared_ptr<ResourcePackFolderModel> resourcePackList() const;
std::shared_ptr<ModFolderModel> texturePackList() const; std::shared_ptr<TexturePackFolderModel> texturePackList() const;
std::shared_ptr<ModFolderModel> shaderPackList() const; std::shared_ptr<ShaderPackFolderModel> shaderPackList() const;
std::shared_ptr<WorldList> worldList() const; std::shared_ptr<WorldList> worldList() const;
std::shared_ptr<GameOptions> gameOptionsModel() const; std::shared_ptr<GameOptions> gameOptionsModel() const;
////// Launch stuff ////// ////// Launch stuff //////
Task::Ptr createUpdateTask(Net::Mode mode) override; Task::Ptr createUpdateTask(Net::Mode mode) override;
shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) 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; QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override;
QList<Mod*> getJarMods() const; QList<Mod*> getJarMods() const;
QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin); QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin);
/// get arguments passed to java /// get arguments passed to java
QStringList javaArguments() const; QStringList javaArguments();
/// get variables for launch command variable substitution/environment /// 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 /// create an environment for launching processes
QProcessEnvironment createEnvironment() override; QProcessEnvironment createEnvironment() override;
@ -103,16 +148,16 @@ public:
QString getStatusbarDescription() override; QString getStatusbarDescription() override;
// FIXME: remove // FIXME: remove
virtual QStringList getClassPath() const; virtual QStringList getClassPath();
// FIXME: remove // FIXME: remove
virtual QStringList getNativeJars() const; virtual QStringList getNativeJars();
// FIXME: remove // FIXME: remove
virtual QString getMainClass() const; virtual QString getMainClass() const;
// FIXME: remove // FIXME: remove
virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) const; virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) const;
virtual JavaVersion getJavaVersion() const; virtual JavaVersion getJavaVersion();
protected: protected:
QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session); QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session);
@ -123,9 +168,9 @@ protected: // data
std::shared_ptr<PackProfile> m_components; std::shared_ptr<PackProfile> m_components;
mutable std::shared_ptr<ModFolderModel> m_loader_mod_list; mutable std::shared_ptr<ModFolderModel> m_loader_mod_list;
mutable std::shared_ptr<ModFolderModel> m_core_mod_list; mutable std::shared_ptr<ModFolderModel> m_core_mod_list;
mutable std::shared_ptr<ModFolderModel> m_resource_pack_list; mutable std::shared_ptr<ResourcePackFolderModel> m_resource_pack_list;
mutable std::shared_ptr<ModFolderModel> m_shader_pack_list; mutable std::shared_ptr<ShaderPackFolderModel> m_shader_pack_list;
mutable std::shared_ptr<ModFolderModel> m_texture_pack_list; mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list;
mutable std::shared_ptr<WorldList> m_world_list; mutable std::shared_ptr<WorldList> m_world_list;
mutable std::shared_ptr<GameOptions> m_game_options; mutable std::shared_ptr<GameOptions> m_game_options;
}; };

View File

@ -43,7 +43,7 @@ void MinecraftUpdate::executeTask()
m_tasks.clear(); m_tasks.clear();
// create folders // create folders
{ {
m_tasks.append(std::make_shared<FoldersTask>(m_inst)); m_tasks.append(new FoldersTask(m_inst));
} }
// add metadata update task if necessary // add metadata update task if necessary
@ -53,23 +53,23 @@ void MinecraftUpdate::executeTask()
auto task = components->getCurrentTask(); auto task = components->getCurrentTask();
if(task) if(task)
{ {
m_tasks.append(task.unwrap()); m_tasks.append(task);
} }
} }
// libraries download // 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 // 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 // assets update
{ {
m_tasks.append(std::make_shared<AssetUpdateTask>(m_inst)); m_tasks.append(new AssetUpdateTask(m_inst));
} }
if(!m_preFailure.isEmpty()) if(!m_preFailure.isEmpty())

View File

@ -50,7 +50,7 @@ private:
private: private:
MinecraftInstance *m_inst = nullptr; MinecraftInstance *m_inst = nullptr;
QList<std::shared_ptr<Task>> m_tasks; QList<Task::Ptr> m_tasks;
QString m_preFailure; QString m_preFailure;
int m_currentTask = -1; int m_currentTask = -1;
bool m_abort = false; bool m_abort = false;

View File

@ -362,11 +362,8 @@ LibraryPtr MojangVersionFormat::libraryFromJson(ProblemContainer & problems, con
{ {
qWarning() << filename << "contains an invalid native (skipping)"; qWarning() << filename << "contains an invalid native (skipping)";
} }
OpSys opSys = OpSys_fromString(it.key()); // FIXME: Skip unknown platforms
if (opSys != Os_Other) out->m_nativeClassifiers[it.key()] = it.value().toString();
{
out->m_nativeClassifiers[opSys] = it.value().toString();
}
} }
} }
if (libObj.contains("rules")) if (libObj.contains("rules"))
@ -395,7 +392,7 @@ QJsonObject MojangVersionFormat::libraryToJson(Library *library)
auto iter = library->m_nativeClassifiers.begin(); auto iter = library->m_nativeClassifiers.begin();
while (iter != library->m_nativeClassifiers.end()) while (iter != library->m_nativeClassifiers.end())
{ {
nativeList.insert(OpSys_toString(iter.key()), iter.value()); nativeList.insert(iter.key(), iter.value());
iter++; iter++;
} }
libRoot.insert("natives", nativeList); libRoot.insert("natives", nativeList);

View File

@ -1,46 +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 "OpSys.h"
OpSys OpSys_fromString(QString name)
{
if (name == "freebsd")
return Os_FreeBSD;
if (name == "linux")
return Os_Linux;
if (name == "windows")
return Os_Windows;
if (name == "osx")
return Os_OSX;
return Os_Other;
}
QString OpSys_toString(OpSys name)
{
switch (name)
{
case Os_FreeBSD:
return "freebsd";
case Os_Linux:
return "linux";
case Os_OSX:
return "osx";
case Os_Windows:
return "windows";
default:
return "other";
}
}

View File

@ -1,38 +0,0 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QString>
enum OpSys
{
Os_Windows,
Os_FreeBSD,
Os_Linux,
Os_OSX,
Os_Other
};
OpSys OpSys_fromString(QString);
QString OpSys_toString(OpSys);
#ifdef Q_OS_WIN32
#define currentSystem Os_Windows
#elif defined Q_OS_MAC
#define currentSystem Os_OSX
#elif defined Q_OS_FREEBSD
#define currentSystem Os_FreeBSD
#else
#define currentSystem Os_Linux
#endif

View File

@ -273,6 +273,11 @@ void PackProfile::scheduleSave()
d->m_saveTimer.start(); d->m_saveTimer.start();
} }
RuntimeContext PackProfile::runtimeContext()
{
return d->m_instance->runtimeContext();
}
QString PackProfile::componentsFilePath() const QString PackProfile::componentsFilePath() const
{ {
return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json"); return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json");
@ -784,7 +789,7 @@ bool PackProfile::removeComponent_internal(ComponentPtr patch)
return true; return true;
} }
QStringList jar, temp1, temp2, temp3; QStringList jar, temp1, temp2, temp3;
jarMod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, d->m_instance->jarmodsPath().absolutePath()); jarMod->getApplicableFiles(d->m_instance->runtimeContext(), jar, temp1, temp2, temp3, d->m_instance->jarmodsPath().absolutePath());
QFileInfo finfo (jar[0]); QFileInfo finfo (jar[0]);
if(finfo.exists()) if(finfo.exists())
{ {

View File

@ -1,4 +1,24 @@
/* Copyright 2013-2021 MultiMC Contributors // 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -104,6 +124,9 @@ public:
/// if there is a save scheduled, do it now. /// if there is a save scheduled, do it now.
void saveNow(); void saveNow();
/// helper method, returns RuntimeContext of instance
RuntimeContext runtimeContext();
signals: signals:
void minecraftChanged(); void minecraftChanged();

View File

@ -1,4 +1,24 @@
/* Copyright 2013-2021 MultiMC Contributors // 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -60,10 +80,10 @@ QList<std::shared_ptr<Rule>> rulesFromJsonV4(const QJsonObject &objectWithRules)
auto osNameVal = osObj.value("name"); auto osNameVal = osObj.value("name");
if (!osNameVal.isString()) if (!osNameVal.isString())
continue; continue;
OpSys requiredOs = OpSys_fromString(osNameVal.toString()); QString osName = osNameVal.toString();
QString versionRegex = osObj.value("version").toString(); QString versionRegex = osObj.value("version").toString();
// add a new OS rule // add a new OS rule
rules.append(OsRule::create(action, requiredOs, versionRegex)); rules.append(OsRule::create(action, osName, versionRegex));
} }
return rules; return rules;
} }
@ -81,7 +101,7 @@ QJsonObject OsRule::toJson()
ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow")); ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow"));
QJsonObject osObj; QJsonObject osObj;
{ {
osObj.insert("name", OpSys_toString(m_system)); osObj.insert("name", m_system);
if(!m_version_regexp.isEmpty()) if(!m_version_regexp.isEmpty())
{ {
osObj.insert("version", m_version_regexp); osObj.insert("version", m_version_regexp);

View File

@ -1,4 +1,24 @@
/* Copyright 2013-2021 MultiMC Contributors // 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,7 +39,7 @@
#include <QList> #include <QList>
#include <QJsonObject> #include <QJsonObject>
#include <memory> #include <memory>
#include "OpSys.h" #include "RuntimeContext.h"
class Library; class Library;
class Rule; class Rule;
@ -37,7 +57,7 @@ class Rule
{ {
protected: protected:
RuleAction m_result; RuleAction m_result;
virtual bool applies(const Library *parent) = 0; virtual bool applies(const Library *parent, const RuntimeContext & runtimeContext) = 0;
public: public:
Rule(RuleAction result) : m_result(result) Rule(RuleAction result) : m_result(result)
@ -45,9 +65,9 @@ public:
} }
virtual ~Rule() {}; virtual ~Rule() {};
virtual QJsonObject toJson() = 0; virtual QJsonObject toJson() = 0;
RuleAction apply(const Library *parent) RuleAction apply(const Library *parent, const RuntimeContext & runtimeContext)
{ {
if (applies(parent)) if (applies(parent, runtimeContext))
return m_result; return m_result;
else else
return Defer; return Defer;
@ -58,23 +78,23 @@ class OsRule : public Rule
{ {
private: private:
// the OS // the OS
OpSys m_system; QString m_system;
// the OS version regexp // the OS version regexp
QString m_version_regexp; QString m_version_regexp;
protected: protected:
virtual bool applies(const Library *) virtual bool applies(const Library *, const RuntimeContext & runtimeContext)
{ {
return (m_system == currentSystem); return runtimeContext.classifierMatches(m_system);
} }
OsRule(RuleAction result, OpSys system, QString version_regexp) OsRule(RuleAction result, QString system, QString version_regexp)
: Rule(result), m_system(system), m_version_regexp(version_regexp) : Rule(result), m_system(system), m_version_regexp(version_regexp)
{ {
} }
public: public:
virtual QJsonObject toJson(); virtual QJsonObject toJson();
static std::shared_ptr<OsRule> create(RuleAction result, OpSys system, static std::shared_ptr<OsRule> create(RuleAction result, QString system,
QString version_regexp) QString version_regexp)
{ {
return std::shared_ptr<OsRule>(new OsRule(result, system, version_regexp)); return std::shared_ptr<OsRule>(new OsRule(result, system, version_regexp));
@ -84,7 +104,7 @@ public:
class ImplicitRule : public Rule class ImplicitRule : public Rule
{ {
protected: protected:
virtual bool applies(const Library *) virtual bool applies(const Library *, const RuntimeContext & runtimeContext)
{ {
return true; return true;
} }

View File

@ -0,0 +1,34 @@
#include "VanillaInstanceCreationTask.h"
#include <utility>
#include "FileSystem.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "settings/INISettingsObject.h"
VanillaCreationTask::VanillaCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loader_version)
: InstanceCreationTask(), m_version(std::move(version)), m_using_loader(true), m_loader(std::move(loader)), m_loader_version(std::move(loader_version))
{}
bool VanillaCreationTask::createInstance()
{
setStatus(tr("Creating instance from version %1").arg(m_version->name()));
auto instance_settings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg"));
instance_settings->suspendSave();
{
MinecraftInstance inst(m_globalSettings, instance_settings, m_stagingPath);
auto components = inst.getPackProfile();
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", m_version->descriptor(), true);
if(m_using_loader)
components->setComponentVersion(m_loader, m_loader_version->descriptor());
inst.setName(name());
inst.setIconKey(m_instIcon);
}
instance_settings->resumeSave();
return true;
}

View File

@ -0,0 +1,22 @@
#pragma once
#include "InstanceCreationTask.h"
#include <utility>
class VanillaCreationTask final : public InstanceCreationTask {
Q_OBJECT
public:
VanillaCreationTask(BaseVersionPtr version) : InstanceCreationTask(), m_version(std::move(version)) {}
VanillaCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loader_version);
bool createInstance() override;
private:
// Version to update to / create of the instance.
BaseVersionPtr m_version;
bool m_using_loader = false;
QString m_loader;
BaseVersionPtr m_loader_version;
};

View File

@ -51,7 +51,7 @@ static bool isMinecraftVersion(const QString &uid)
return uid == "net.minecraft"; return uid == "net.minecraft";
} }
void VersionFile::applyTo(LaunchProfile *profile) void VersionFile::applyTo(LaunchProfile *profile, const RuntimeContext & runtimeContext)
{ {
// Only real Minecraft can set those. Don't let anything override them. // Only real Minecraft can set those. Don't let anything override them.
if (isMinecraftVersion(uid)) if (isMinecraftVersion(uid))
@ -77,15 +77,15 @@ void VersionFile::applyTo(LaunchProfile *profile)
for (auto library : libraries) for (auto library : libraries)
{ {
profile->applyLibrary(library); profile->applyLibrary(library, runtimeContext);
} }
for (auto mavenFile : mavenFiles) for (auto mavenFile : mavenFiles)
{ {
profile->applyMavenFile(mavenFile); profile->applyMavenFile(mavenFile, runtimeContext);
} }
for (auto agent : agents) for (auto agent : agents)
{ {
profile->applyAgent(agent); profile->applyAgent(agent, runtimeContext);
} }
profile->applyProblemSeverity(getProblemSeverity()); profile->applyProblemSeverity(getProblemSeverity());
} }

View File

@ -41,7 +41,6 @@
#include <QSet> #include <QSet>
#include <memory> #include <memory>
#include "minecraft/OpSys.h"
#include "minecraft/Rule.h" #include "minecraft/Rule.h"
#include "ProblemProvider.h" #include "ProblemProvider.h"
#include "Library.h" #include "Library.h"
@ -60,7 +59,7 @@ class VersionFile : public ProblemContainer
friend class MojangVersionFormat; friend class MojangVersionFormat;
friend class OneSixVersionFormat; friend class OneSixVersionFormat;
public: /* methods */ public: /* methods */
void applyTo(LaunchProfile* profile); void applyTo(LaunchProfile* profile, const RuntimeContext & runtimeContext);
public: /* data */ public: /* data */
/// SneedMC: order hint for this version file if no explicit order is set /// SneedMC: order hint for this version file if no explicit order is set

View File

@ -53,12 +53,12 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <nonstd/optional> #include <optional>
using nonstd::optional; using std::optional;
using nonstd::nullopt; using std::nullopt;
GameType::GameType(nonstd::optional<int> original): GameType::GameType(std::optional<int> original):
original(original) original(original)
{ {
if(!original) { if(!original) {

View File

@ -16,11 +16,11 @@
#pragma once #pragma once
#include <QFileInfo> #include <QFileInfo>
#include <QDateTime> #include <QDateTime>
#include <nonstd/optional> #include <optional>
struct GameType { struct GameType {
GameType() = default; GameType() = default;
GameType (nonstd::optional<int> original); GameType (std::optional<int> original);
QString toTranslatedString() const; QString toTranslatedString() const;
QString toLogString() const; QString toLogString() const;
@ -33,7 +33,7 @@ struct GameType {
Adventure, Adventure,
Spectator Spectator
} type = Unknown; } type = Unknown;
nonstd::optional<int> original; std::optional<int> original;
}; };
class World class World

View File

@ -109,8 +109,10 @@ QStringList AccountList::profileNames() const {
void AccountList::addAccount(const MinecraftAccountPtr account) void AccountList::addAccount(const MinecraftAccountPtr account)
{ {
// NOTE: Do not allow adding something that's already there // NOTE: Do not allow adding something that's already there. We shouldn't let it continue
// because of the signal / slot connections after this.
if (m_accounts.contains(account)) { if (m_accounts.contains(account)) {
qDebug() << "Tried to add account that's already on the accounts list!";
return; return;
} }
@ -123,6 +125,8 @@ void AccountList::addAccount(const MinecraftAccountPtr account)
if(profileId.size()) { if(profileId.size()) {
auto existingAccount = findAccountByProfileId(profileId); auto existingAccount = findAccountByProfileId(profileId);
if(existingAccount != -1) { if(existingAccount != -1) {
qDebug() << "Replacing old account with a new one with the same profile ID!";
MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount]; MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount];
m_accounts[existingAccount] = account; m_accounts[existingAccount] = account;
if(m_defaultAccount == existingAccountPtr) { if(m_defaultAccount == existingAccountPtr) {
@ -138,9 +142,12 @@ void AccountList::addAccount(const MinecraftAccountPtr account)
// if we don't have this profileId yet, add the account to the end // if we don't have this profileId yet, add the account to the end
int row = m_accounts.count(); int row = m_accounts.count();
qDebug() << "Inserting account at index" << row;
beginInsertRows(QModelIndex(), row, row); beginInsertRows(QModelIndex(), row, row);
m_accounts.append(account); m_accounts.append(account);
endInsertRows(); endInsertRows();
onListChanged(); onListChanged();
} }

View File

@ -238,7 +238,7 @@ void MinecraftAccount::authFailed(QString reason)
} }
bool MinecraftAccount::isActive() const { bool MinecraftAccount::isActive() const {
return m_currentTask; return !m_currentTask.isNull();
} }
bool MinecraftAccount::shouldRefresh() const { bool MinecraftAccount::shouldRefresh() const {

View File

@ -21,6 +21,8 @@
#include <FileSystem.h> #include <FileSystem.h>
#include <Commandline.h> #include <Commandline.h>
#include "Application.h"
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
#include "gamemode_client.h" #include "gamemode_client.h"
#endif #endif
@ -86,7 +88,7 @@ void DirectJavaLaunch::executeTask()
} }
#ifdef Q_OS_LINUX #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(); auto pid = m_process.processId();
if (pid) if (pid)

View File

@ -181,7 +181,7 @@ void LauncherPartLaunch::executeTask()
} }
#ifdef Q_OS_LINUX #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(); auto pid = m_process.processId();
if (pid) if (pid)

View File

@ -1,4 +1,24 @@
/* Copyright 2013-2021 MultiMC Contributors // 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,7 +36,6 @@
#include "ModMinecraftJar.h" #include "ModMinecraftJar.h"
#include "launch/LaunchTask.h" #include "launch/LaunchTask.h"
#include "MMCZip.h" #include "MMCZip.h"
#include "minecraft/OpSys.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
@ -50,7 +69,7 @@ void ModMinecraftJar::executeTask()
{ {
auto mainJar = profile->getMainJar(); auto mainJar = profile->getMainJar();
QStringList jars, temp1, temp2, temp3, temp4; QStringList jars, temp1, temp2, temp3, temp4;
mainJar->getApplicableFiles(currentSystem, jars, temp1, temp2, temp3, m_inst->getLocalLibraryPath()); mainJar->getApplicableFiles(m_inst->runtimeContext(), jars, temp1, temp2, temp3, m_inst->getLocalLibraryPath());
auto sourceJarPath = jars[0]; auto sourceJarPath = jars[0];
if(!MMCZip::createModdedJar(sourceJarPath, finalJarPath, jarMods)) if(!MMCZip::createModdedJar(sourceJarPath, finalJarPath, jarMods))
{ {

View File

@ -1,4 +1,24 @@
/* Copyright 2013-2021 MultiMC Contributors // 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,7 +36,6 @@
#include "ScanModFolders.h" #include "ScanModFolders.h"
#include "launch/LaunchTask.h" #include "launch/LaunchTask.h"
#include "MMCZip.h" #include "MMCZip.h"
#include "minecraft/OpSys.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ModFolderModel.h"

View File

@ -36,130 +36,77 @@
#include "Mod.h" #include "Mod.h"
#include <QDebug>
#include <QDir> #include <QDir>
#include <QString> #include <QString>
#include <QRegularExpression>
#include <FileSystem.h>
#include <QDebug>
#include "Application.h"
#include "MetadataHandler.h" #include "MetadataHandler.h"
#include "Version.h"
namespace { Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
ModDetails invalidDetails;
}
Mod::Mod(const QFileInfo& file)
{ {
repath(file); m_enabled = (file.suffix() != "disabled");
m_changedDateTime = file.lastModified();
} }
Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata) Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata)
: m_file(mods_dir.absoluteFilePath(metadata.filename)) : Mod(mods_dir.absoluteFilePath(metadata.filename))
, m_internal_id(metadata.filename)
, m_name(metadata.name)
{ {
if (m_file.isDir()) { m_name = metadata.name;
m_type = MOD_FOLDER; m_local_details.metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
} 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));
}
void Mod::repath(const QFileInfo& file)
{
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;
}
}
auto Mod::enable(bool value) -> bool
{
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));
m_enabled = value;
return true;
} }
void Mod::setStatus(ModStatus status) void Mod::setStatus(ModStatus status)
{ {
if (m_localDetails) { m_local_details.status = status;
m_localDetails->status = status;
} else {
m_temp_status = status;
} }
} void Mod::setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata)
void Mod::setMetadata(const Metadata::ModStruct& metadata)
{ {
if (status() == ModStatus::NoMetadata) if (status() == ModStatus::NoMetadata)
setStatus(ModStatus::Installed); setStatus(ModStatus::Installed);
if (m_localDetails) { m_local_details.metadata = metadata;
m_localDetails->metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
} else {
m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
} }
std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
{
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 };
}
bool Mod::applyFilter(QRegularExpression filter) const
{
if (filter.match(description()).hasMatch())
return true;
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 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 Resource::destroy();
return FS::deletePath(m_file.filePath());
} }
auto Mod::details() const -> const ModDetails& auto Mod::details() const -> const ModDetails&
{ {
return m_localDetails ? *m_localDetails : invalidDetails; return m_local_details;
} }
auto Mod::name() const -> QString auto Mod::name() const -> QString
@ -218,35 +164,29 @@ auto Mod::authors() const -> QStringList
auto Mod::status() const -> ModStatus auto Mod::status() const -> ModStatus
{ {
if (!m_localDetails)
return m_temp_status;
return details().status; return details().status;
} }
auto Mod::metadata() -> std::shared_ptr<Metadata::ModStruct> auto Mod::metadata() -> std::shared_ptr<Metadata::ModStruct>
{ {
if (m_localDetails) return m_local_details.metadata;
return m_localDetails->metadata;
return m_temp_metadata;
} }
auto Mod::metadata() const -> const std::shared_ptr<Metadata::ModStruct> auto Mod::metadata() const -> const std::shared_ptr<Metadata::ModStruct>
{ {
if (m_localDetails) return m_local_details.metadata;
return m_localDetails->metadata;
return m_temp_metadata;
} }
void Mod::finishResolvingWithDetails(std::shared_ptr<ModDetails> details) void Mod::finishResolvingWithDetails(ModDetails&& details)
{ {
m_resolving = false; m_is_resolving = false;
m_resolved = true; m_is_resolved = true;
m_localDetails = details;
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()) { m_local_details = std::move(details);
setMetadata(*m_temp_metadata); if (metadata)
m_temp_metadata.reset(); setMetadata(std::move(metadata));
}
} }

View File

@ -39,38 +39,23 @@
#include <QFileInfo> #include <QFileInfo>
#include <QList> #include <QList>
#include "QObjectPtr.h" #include "Resource.h"
#include "ModDetails.h" #include "ModDetails.h"
class Mod : public QObject class Mod : public Resource
{ {
Q_OBJECT Q_OBJECT
public: 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 Ptr = shared_qobject_ptr<Mod>;
using WeakPtr = QPointer<Mod>;
Mod() = default; Mod() = default;
Mod(const QFileInfo &file); Mod(const QFileInfo &file);
explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata); Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
Mod(QString file_path) : Mod(QFileInfo(file_path)) {}
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; }
auto details() const -> const ModDetails&; auto details() const -> const ModDetails&;
auto name() const -> QString; auto name() const -> QString override;
auto version() const -> QString; auto version() const -> QString;
auto homeurl() const -> QString; auto homeurl() const -> QString;
auto description() const -> QString; auto description() const -> QString;
@ -81,46 +66,17 @@ public:
auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>; auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>;
void setStatus(ModStatus status); 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; auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool;
// change the mod's filesystem path (used by mod lists for *MAGIC* purposes) void finishResolvingWithDetails(ModDetails&& details);
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);
protected: protected:
QFileInfo m_file; ModDetails m_local_details;
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;
}; };

View File

@ -46,34 +46,77 @@ enum class ModStatus {
Installed, // Both JAR and Metadata are present Installed, // Both JAR and Metadata are present
NotInstalled, // Only the Metadata is present NotInstalled, // Only the Metadata is present
NoMetadata, // Only the JAR is present NoMetadata, // Only the JAR is present
Unknown, // Default status
}; };
struct ModDetails struct ModDetails
{ {
/* Mod ID as defined in the ModLoader-specific metadata */ /* Mod ID as defined in the ModLoader-specific metadata */
QString mod_id; QString mod_id = {};
/* Human-readable name */ /* Human-readable name */
QString name; QString name = {};
/* Human-readable mod version */ /* Human-readable mod version */
QString version; QString version = {};
/* Human-readable minecraft version */ /* Human-readable minecraft version */
QString mcversion; QString mcversion = {};
/* URL for mod's home page */ /* URL for mod's home page */
QString homeurl; QString homeurl = {};
/* Human-readable description */ /* Human-readable description */
QString description; QString description = {};
/* List of the author's names */ /* List of the author's names */
QStringList authors; QStringList authors = {};
/* Installation status of the mod */ /* Installation status of the mod */
ModStatus status; ModStatus status = ModStatus::Unknown;
/* Metadata information, if any */ /* 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;
}
}; };

View File

@ -49,428 +49,52 @@
#include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/LocalModParseTask.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.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_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE };
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;
} }
QVariant ModFolderModel::data(const QModelIndex &index, int role) const QVariant ModFolderModel::data(const QModelIndex &index, int role) const
{ {
if (!index.isValid()) if (!validateIndex(index))
return QVariant(); return {};
int row = index.row(); int row = index.row();
int column = index.column(); int column = index.column();
if (row < 0 || row >= mods.size())
return QVariant();
switch (role) switch (role)
{ {
case Qt::DisplayRole: case Qt::DisplayRole:
switch (column) switch (column)
{ {
case NameColumn: case NameColumn:
return mods[row]->name(); return m_resources[row]->name();
case VersionColumn: { case VersionColumn: {
switch(mods[row]->type()) { switch(m_resources[row]->type()) {
case Mod::MOD_FOLDER: case ResourceType::FOLDER:
return tr("Folder"); return tr("Folder");
case Mod::MOD_SINGLEFILE: case ResourceType::SINGLEFILE:
return tr("File"); return tr("File");
default: default:
break; break;
} }
return mods[row]->version(); return at(row)->version();
} }
case DateColumn: case DateColumn:
return mods[row]->dateTimeChanged(); return m_resources[row]->dateTimeChanged();
default: default:
return QVariant(); return QVariant();
} }
case Qt::ToolTipRole: case Qt::ToolTipRole:
return mods[row]->internal_id(); return m_resources[row]->internal_id();
case Qt::CheckStateRole: case Qt::CheckStateRole:
switch (column) switch (column)
{ {
case ActiveColumn: case ActiveColumn:
return mods[row]->enabled() ? Qt::Checked : Qt::Unchecked; return at(row)->enabled() ? Qt::Checked : Qt::Unchecked;
default: default:
return QVariant(); return QVariant();
} }
@ -479,61 +103,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 QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
{ {
switch (role) switch (role)
@ -573,65 +142,142 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in
return QVariant(); return QVariant();
} }
Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const int ModFolderModel::columnCount(const QModelIndex &parent) const
{ {
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); return NUM_COLUMNS;
auto flags = defaultFlags;
if(interaction_disabled) {
flags &= ~Qt::ItemIsDropEnabled;
}
else
{
flags |= Qt::ItemIsDropEnabled;
if(index.isValid()) {
flags |= Qt::ItemIsUserCheckable;
}
}
return flags;
} }
Qt::DropActions ModFolderModel::supportedDropActions() const Task* ModFolderModel::createUpdateTask()
{ {
// copy from outside, move from within and other mod lists auto index_dir = indexDir();
return Qt::CopyAction | Qt::MoveAction; 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; return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo());
types << "text/uri-list";
return types;
} }
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)
{ {
for(auto mod : allMods()){
if(mod->fileinfo().fileName() == filename){
auto index_dir = indexDir();
mod->destroy(index_dir, preserve_metadata);
update();
return true; return true;
} }
}
// check if the action is supported
if (!data || !(action & supportedDropActions()))
{
return false; return false;
} }
// files dropped from outside? bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
if (data->hasUrls())
{ {
auto urls = data->urls(); if(!m_can_interact) {
for (auto url : urls) return false;
{ }
// only local files may be dropped...
if (!url.isLocalFile()) if(indexes.isEmpty())
return true;
for (auto i: indexes)
{ {
if(i.column() != 0) {
continue; continue;
} }
// TODO: implement not only copy, but also move auto m = at(i.row());
// FIXME: handle errors here auto index_dir = indexDir();
installMod(url.toLocalFile()); m->destroy(index_dir);
} }
update();
return true; return true;
} }
return false;
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 : qAsConst(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));
} }

View File

@ -44,6 +44,7 @@
#include <QAbstractListModel> #include <QAbstractListModel>
#include "Mod.h" #include "Mod.h"
#include "ResourceFolderModel.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/LocalModParseTask.h"
@ -56,7 +57,7 @@ class QFileSystemWatcher;
* A legacy mod list. * A legacy mod list.
* Backed by a folder. * Backed by a folder.
*/ */
class ModFolderModel : public QAbstractListModel class ModFolderModel : public ResourceFolderModel
{ {
Q_OBJECT Q_OBJECT
public: public:
@ -75,105 +76,38 @@ public:
}; };
ModFolderModel(const QString &dir, bool is_indexed = false); ModFolderModel(const QString &dir, bool is_indexed = false);
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; 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;
/// flags, mostly to support drag&drop QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
virtual Qt::ItemFlags flags(const QModelIndex &index) const override; int columnCount(const QModelIndex &parent) const override;
QStringList mimeTypes() const override;
bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override;
virtual int rowCount(const QModelIndex &) const override [[nodiscard]] Task* createUpdateTask() override;
{ [[nodiscard]] Task* createParseTask(Resource&) 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);
bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); }
bool uninstallMod(const QString& filename, bool preserve_metadata = false); bool uninstallMod(const QString& filename, bool preserve_metadata = false);
/// Deletes all the selected mods /// Deletes all the selected mods
bool deleteMods(const QModelIndexList &indexes); bool deleteMods(const QModelIndexList &indexes);
/// Enable or disable listed mods
bool setModStatus(const QModelIndexList &indexes, ModStatusAction action);
void startWatching();
void stopWatching();
bool isValid(); bool isValid();
QDir& dir() bool startWatching() override;
{ bool stopWatching() override;
return m_dir;
}
QDir indexDir() QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; }
{
return { QString("%1/.index").arg(dir().absolutePath()) };
}
const QList<Mod::Ptr>& allMods() auto selectedMods(QModelIndexList& indexes) -> QList<Mod*>;
{ auto allMods() -> QList<Mod*>;
return mods;
}
auto selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>; RESOURCE_HELPERS(Mod)
public slots:
void disableInteraction(bool disabled);
private private
slots: slots:
void directoryChanged(QString path); void onUpdateSucceeded() override;
void finishUpdate(); void onParseSucceeded(int ticket, QString resource_id) override;
void finishModParse(int token);
signals:
void updateFinished();
private:
void resolveMod(Mod::Ptr m);
bool setModStatus(int index, ModStatusAction action);
protected: 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; bool m_is_indexed;
QMap<QString, int> modsIndex; bool m_first_folder_load = true;
QMap<int, LocalModParseTask::ResultPtr> activeTickets;
int nextResolutionTicket = 0;
QList<Mod::Ptr> mods;
}; };

View File

@ -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"

View File

@ -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());
}

View File

@ -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;
};

View File

@ -0,0 +1,526 @@
#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)
{
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);
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* 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);
}

View File

@ -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* 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.at(row);
if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) {
// no significant change, ignore...
continue;
}
// If the resource is resolving, but something about it changed, we don't want to
// continue the resolving.
if (current_resource->isResolving()) {
auto ticket = current_resource->resolutionTicket();
if (m_active_parse_tasks.contains(ticket)) {
auto task = (*m_active_parse_tasks.find(ticket)).get();
task->abort();
}
}
m_resources[row].reset(new_resource);
resolveResource(m_resources.at(row).get());
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(m_resources.last().get());
}
endInsertRows();
}
}
// update index
{
m_resources_index.clear();
int idx = 0;
for (auto const& mod : qAsConst(m_resources)) {
m_resources_index[mod->internal_id()] = idx;
idx++;
}
}
}

View File

@ -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);
}

View File

@ -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;
};

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* PolyMC - Minecraft Launcher * PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -35,24 +36,116 @@
#include "ResourcePackFolderModel.h" #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 { QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
if (role == Qt::ToolTipRole) { {
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) { switch (section) {
case ActiveColumn: case ActiveColumn:
return tr("Is the resource pack enabled?"); 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: case NameColumn:
return tr("The name of the resource pack."); return tr("The name of the resource pack.");
case VersionColumn: case PackFormatColumn:
return tr("The version of the resource pack."); //: 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: case DateColumn:
return tr("The date and time this resource pack was last changed (or added)."); return tr("The date and time this resource pack was last changed (or added).");
default: default:
return QVariant(); return {};
} }
default:
return {};
}
return {};
} }
return ModFolderModel::headerData(section, orientation, role); 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); });
}
Task* ResourcePackFolderModel::createParseTask(Resource& resource)
{
return new LocalResourcePackParseTask(m_next_resolution_ticket, static_cast<ResourcePack&>(resource));
} }

View File

@ -1,13 +1,31 @@
#pragma once #pragma once
#include "ModFolderModel.h" #include "ResourceFolderModel.h"
class ResourcePackFolderModel : public ModFolderModel #include "ResourcePack.h"
class ResourcePackFolderModel : public ResourceFolderModel
{ {
Q_OBJECT Q_OBJECT
public: public:
enum Columns
{
ActiveColumn = 0,
NameColumn,
PackFormatColumn,
DateColumn,
NUM_COLUMNS
};
explicit ResourcePackFolderModel(const QString &dir); 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)
}; };

View File

@ -0,0 +1,10 @@
#pragma once
#include "ResourceFolderModel.h"
class ShaderPackFolderModel : public ResourceFolderModel {
Q_OBJECT
public:
explicit ShaderPackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir)) {}
};

View File

@ -0,0 +1,64 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* 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/>.
*/
#include "TexturePack.h"
#include <QDebug>
#include <QMap>
#include <QRegularExpression>
#include "minecraft/mod/tasks/LocalTexturePackParseTask.h"
void TexturePack::setDescription(QString new_description)
{
QMutexLocker locker(&m_data_lock);
m_description = new_description;
}
void TexturePack::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 TexturePack::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.
TexturePackUtils::process(*this);
return image(size);
}

View File

@ -0,0 +1,67 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* 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/>.
*/
#pragma once
#include "Resource.h"
#include <QImage>
#include <QMutex>
#include <QPixmap>
#include <QPixmapCache>
class Version;
class TexturePack : public Resource {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<Resource>;
TexturePack(QObject* parent = nullptr) : Resource(parent) {}
TexturePack(QFileInfo file_info) : Resource(file_info) {}
/** Gets the description of the texture pack. */
[[nodiscard]] QString description() const { return m_description; }
/** Gets the image of the texture pack, converted to a QPixmap for drawing, and scaled to size. */
[[nodiscard]] QPixmap image(QSize size);
/** Thread-safe. */
void setDescription(QString new_description);
/** Thread-safe. */
void setImage(QImage new_image);
protected:
mutable QMutex m_data_lock;
/** The texture pack's description, as defined in the pack.txt file.
*/
QString m_description;
/** The texture 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;
};

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* PolyMC - Minecraft Launcher * PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -35,24 +36,17 @@
#include "TexturePackFolderModel.h" #include "TexturePackFolderModel.h"
TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) { #include "minecraft/mod/tasks/BasicFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalTexturePackParseTask.h"
TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}
Task* TexturePackFolderModel::createUpdateTask()
{
return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return new TexturePack(entry); });
} }
QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { Task* TexturePackFolderModel::createParseTask(Resource& resource)
if (role == Qt::ToolTipRole) { {
switch (section) { return new LocalTexturePackParseTask(m_next_resolution_ticket, static_cast<TexturePack&>(resource));
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);
} }

View File

@ -1,13 +1,49 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* 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.
*/
#pragma once #pragma once
#include "ModFolderModel.h" #include "ResourceFolderModel.h"
class TexturePackFolderModel : public ModFolderModel class TexturePackFolderModel : public ResourceFolderModel
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit TexturePackFolderModel(const QString &dir); explicit TexturePackFolderModel(const QString &dir);
[[nodiscard]] Task* createUpdateTask() override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override; [[nodiscard]] Task* createParseTask(Resource&) override;
}; };

View File

@ -0,0 +1,72 @@
#pragma once
#include <QDir>
#include <QMap>
#include <QObject>
#include <QThread>
#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_thread_to_spawn_into(thread())
{
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)), m_thread_to_spawn_into(thread())
{}
[[nodiscard]] bool canAbort() const override { return true; }
bool abort() override
{
m_aborted.store(true);
return true;
}
void executeTask() override
{
if (thread() != m_thread_to_spawn_into)
connect(this, &Task::finished, this->thread(), &QThread::quit);
m_dir.refresh();
for (auto entry : m_dir.entryInfoList()) {
auto resource = m_create_func(entry);
resource->moveToThread(m_thread_to_spawn_into);
m_result->resources.insert(resource->internal_id(), resource);
}
if (m_aborted)
emit finished();
else
emitSucceeded();
}
private:
QDir m_dir;
ResultPtr m_result;
std::atomic<bool> m_aborted = false;
std::function<Resource*(QFileInfo const&)> m_create_func;
/** This is the thread in which we should put new mod objects */
QThread* m_thread_to_spawn_into;
};

View File

@ -1,17 +1,17 @@
#include "LocalModParseTask.h" #include "LocalModParseTask.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include <QString>
#include <quazip/quazip.h> #include <quazip/quazip.h>
#include <quazip/quazipfile.h> #include <quazip/quazipfile.h>
#include <toml.h> #include <toml++/toml.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QString>
#include "FileSystem.h"
#include "Json.h" #include "Json.h"
#include "settings/INIFile.h" #include "settings/INIFile.h"
#include "FileSystem.h"
namespace { namespace {
@ -20,54 +20,47 @@ namespace {
// OLD format: // OLD format:
// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc // 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()) { 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(); 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(); auto name = firstObj.value("name").toString();
// NOTE: ignore stupid example mods copies where the author didn't even bother to change the name // NOTE: ignore stupid example mods copies where the author didn't even bother to change the name
if (name != "Example Mod") { 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(); auto homeurl = firstObj.value("url").toString().trimmed();
if(!homeurl.isEmpty()) if (!homeurl.isEmpty()) {
{
// fix up url. // fix up url.
if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) {
{
homeurl.prepend("http://"); homeurl.prepend("http://");
} }
} }
details->homeurl = homeurl; details.homeurl = homeurl;
details->description = firstObj.value("description").toString(); details.description = firstObj.value("description").toString();
QJsonArray authors = firstObj.value("authorList").toArray(); QJsonArray authors = firstObj.value("authorList").toArray();
if (authors.size() == 0) { if (authors.size() == 0) {
// FIXME: what is the format of this? is there any? // FIXME: what is the format of this? is there any?
authors = firstObj.value("authors").toArray(); authors = firstObj.value("authors").toArray();
} }
for (auto author: authors) for (auto author : authors) {
{ details.authors.append(author.toString());
details->authors.append(author.toString());
} }
return details; return details;
}; };
QJsonParseError jsonError; QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
// this is the very old format that had just the array // this is the very old format that had just the array
if (jsonDoc.isArray()) if (jsonDoc.isArray()) {
{
return getInfoFromArray(jsonDoc.array()); return getInfoFromArray(jsonDoc.array());
} } else if (jsonDoc.isObject()) {
else if (jsonDoc.isObject())
{
auto val = jsonDoc.object().value("modinfoversion"); auto val = jsonDoc.object().value("modinfoversion");
if (val.isUndefined()) { if (val.isUndefined()) {
val = jsonDoc.object().value("modListVersion"); val = jsonDoc.object().value("modListVersion");
@ -79,171 +72,131 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
if (version < 0) if (version < 0)
version = Json::ensureString(val, "").toInt(); version = Json::ensureString(val, "").toInt();
if (version != 2) if (version != 2) {
{
qCritical() << "BAD stuff happened to mod json:"; qCritical() << "BAD stuff happened to mod json:";
qCritical() << contents; qCritical() << contents;
return nullptr; return {};
} }
auto arrVal = jsonDoc.object().value("modlist"); auto arrVal = jsonDoc.object().value("modlist");
if (arrVal.isUndefined()) { if (arrVal.isUndefined()) {
arrVal = jsonDoc.object().value("modList"); arrVal = jsonDoc.object().value("modList");
} }
if (arrVal.isArray()) if (arrVal.isArray()) {
{
return getInfoFromArray(arrVal.toArray()); return getInfoFromArray(arrVal.toArray());
} }
} }
return nullptr; return {};
} }
// https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md // 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]; toml::table tomlData;
// top-level table #if TOML_EXCEPTIONS
toml_table_t* tomlData = toml_parse(contents.data(), errbuf, sizeof(errbuf)); try {
tomlData = toml::parse(contents.toStdString());
if(!tomlData) } catch (const toml::parse_error& err) {
{ return {};
return nullptr;
} }
#else
tomlData = toml::parse(contents.toStdString());
if (!tomlData) {
return {};
}
#endif
// array defined by [[mods]] // array defined by [[mods]]
toml_array_t* tomlModsArr = toml_array_in(tomlData, "mods"); auto tomlModsArr = tomlData["mods"].as_array();
if(!tomlModsArr) if (!tomlModsArr) {
{
qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!"; 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 // we only really care about the first element, since multiple mods in one file is not supported by us at the moment
toml_table_t* tomlModsTable0 = toml_table_at(tomlModsArr, 0); auto tomlModsTable0 = tomlModsArr->get(0);
if(!tomlModsTable0) if (!tomlModsTable0) {
{
qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!"; qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!";
return nullptr; return {};
}
auto modsTable = tomlModsTable0->as_table();
if (!tomlModsTable0) {
qWarning() << "Corrupted mods.toml? [[mods]] was not a table!";
return {};
} }
// mandatory properties - always in [[mods]] // mandatory properties - always in [[mods]]
toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId"); if (auto modIdDatum = (*modsTable)["modId"].as_string()) {
if(modIdDatum.ok) details.mod_id = QString::fromStdString(modIdDatum->get());
{
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 (auto versionDatum = (*modsTable)["version"].as_string()) {
if(versionDatum.ok) details.version = QString::fromStdString(versionDatum->get());
{
details->version = versionDatum.u.s;
free(versionDatum.u.s);
} }
toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName"); if (auto displayNameDatum = (*modsTable)["displayName"].as_string()) {
if(displayNameDatum.ok) details.name = QString::fromStdString(displayNameDatum->get());
{
details->name = displayNameDatum.u.s;
free(displayNameDatum.u.s);
} }
toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description"); if (auto descriptionDatum = (*modsTable)["description"].as_string()) {
if(descriptionDatum.ok) details.description = QString::fromStdString(descriptionDatum->get());
{
details->description = descriptionDatum.u.s;
free(descriptionDatum.u.s);
} }
// optional properties - can be in the root table or [[mods]] // optional properties - can be in the root table or [[mods]]
toml_datum_t authorsDatum = toml_string_in(tomlData, "authors");
QString authors = ""; QString authors = "";
if(authorsDatum.ok) if (auto authorsDatum = tomlData["authors"].as_string()) {
{ authors = QString::fromStdString(authorsDatum->get());
authors = authorsDatum.u.s; } else if (auto authorsDatum = (*modsTable)["authors"].as_string()) {
free(authorsDatum.u.s); authors = QString::fromStdString(authorsDatum->get());
} }
else if (!authors.isEmpty()) {
{ details.authors.append(authors);
authorsDatum = toml_string_in(tomlModsTable0, "authors");
if(authorsDatum.ok)
{
authors = authorsDatum.u.s;
free(authorsDatum.u.s);
}
}
if(!authors.isEmpty())
{
details->authors.append(authors);
} }
toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL");
QString homeurl = ""; QString homeurl = "";
if(homeurlDatum.ok) if (auto homeurlDatum = tomlData["displayURL"].as_string()) {
{ homeurl = QString::fromStdString(homeurlDatum->get());
homeurl = homeurlDatum.u.s; } else if (auto homeurlDatum = (*modsTable)["displayURL"].as_string()) {
free(homeurlDatum.u.s); homeurl = QString::fromStdString(homeurlDatum->get());
} }
else
{
homeurlDatum = toml_string_in(tomlModsTable0, "displayURL");
if(homeurlDatum.ok)
{
homeurl = homeurlDatum.u.s;
free(homeurlDatum.u.s);
}
}
if(!homeurl.isEmpty())
{
// fix up url. // fix up url.
if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) if (!homeurl.isEmpty() && !homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) {
{
homeurl.prepend("http://"); homeurl.prepend("http://");
} }
} details.homeurl = homeurl;
details->homeurl = homeurl;
// this seems to be recursive, so it should free everything
toml_free(tomlData);
return details; return details;
} }
// https://fabricmc.net/wiki/documentation:fabric_mod_json // https://fabricmc.net/wiki/documentation:fabric_mod_json
std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents) ModDetails ReadFabricModInfo(QByteArray contents)
{ {
QJsonParseError jsonError; QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object(); auto object = jsonDoc.object();
auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0; 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.mod_id = object.value("id").toString();
details->version = object.value("version").toString(); details.version = object.value("version").toString();
details->name = object.contains("name") ? object.value("name").toString() : details->mod_id; details.name = object.contains("name") ? object.value("name").toString() : details.mod_id;
details->description = object.value("description").toString(); details.description = object.value("description").toString();
if (schemaVersion >= 1) if (schemaVersion >= 1) {
{
QJsonArray authors = object.value("authors").toArray(); QJsonArray authors = object.value("authors").toArray();
for (auto author: authors) for (auto author : authors) {
{
if (author.isObject()) { if (author.isObject()) {
details->authors.append(author.toObject().value("name").toString()); details.authors.append(author.toObject().value("name").toString());
} } else {
else { details.authors.append(author.toString());
details->authors.append(author.toString());
} }
} }
if (object.contains("contact")) if (object.contains("contact")) {
{
QJsonObject contact = object.value("contact").toObject(); QJsonObject contact = object.value("contact").toObject();
if (contact.contains("homepage")) if (contact.contains("homepage")) {
{ details.homeurl = contact.value("homepage").toString();
details->homeurl = contact.value("homepage").toString();
} }
} }
} }
@ -251,50 +204,48 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
} }
// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md // 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; QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = Json::requireObject(jsonDoc, "quilt.mod.json"); auto object = Json::requireObject(jsonDoc, "quilt.mod.json");
auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version"); 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 // https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md
if (schemaVersion == 1) if (schemaVersion == 1) {
{
auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info"); auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info");
details->mod_id = Json::requireString(modInfo.value("id"), "Mod ID"); details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
details->version = Json::requireString(modInfo.value("version"), "Mod version"); details.version = Json::requireString(modInfo.value("version"), "Mod version");
auto modMetadata = Json::ensureObject(modInfo.value("metadata")); auto modMetadata = Json::ensureObject(modInfo.value("metadata"));
details->name = Json::ensureString(modMetadata.value("name"), details->mod_id); details.name = Json::ensureString(modMetadata.value("name"), details.mod_id);
details->description = Json::ensureString(modMetadata.value("description")); details.description = Json::ensureString(modMetadata.value("description"));
auto modContributors = Json::ensureObject(modMetadata.value("contributors")); auto modContributors = Json::ensureObject(modMetadata.value("contributors"));
// We don't really care about the role of a contributor here // 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")); auto modContact = Json::ensureObject(modMetadata.value("contact"));
if (modContact.contains("homepage")) if (modContact.contains("homepage")) {
{ details.homeurl = Json::requireString(modContact.value("homepage"));
details->homeurl = Json::requireString(modContact.value("homepage"));
} }
} }
return details; 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 // Read the data
details->name = "Minecraft Forge"; details.name = "Minecraft Forge";
details->mod_id = "Forge"; details.mod_id = "Forge";
details->homeurl = "http://www.minecraftforge.net/forum/"; details.homeurl = "http://www.minecraftforge.net/forum/";
INIFile ini; INIFile ini;
if (!ini.loadFile(contents)) if (!ini.loadFile(contents))
return details; return details;
@ -304,47 +255,39 @@ std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
QString revision = ini.get("forge.revision.number", "0").toString(); QString revision = ini.get("forge.revision.number", "0").toString();
QString build = ini.get("forge.build.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; 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; QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object(); auto object = jsonDoc.object();
if (object.contains("name")) 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")) if (object.contains("version")) {
{ details.version = object.value("version").toString("");
details->version = object.value("version").toString(""); } else {
details.version = object.value("revision").toString("");
} }
else details.mcversion = object.value("mcversion").toString();
{
details->version = object.value("revision").toString("");
}
details->mcversion = object.value("mcversion").toString();
auto author = object.value("author").toString(); auto author = object.value("author").toString();
if (!author.isEmpty()) { if (!author.isEmpty()) {
details->authors.append(author); details.authors.append(author);
} }
details->description = object.value("description").toString(); details.description = object.value("description").toString();
details->homeurl = object.value("url").toString(); details.homeurl = object.value("url").toString();
return details; return details;
} }
} } // namespace
LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile): LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile)
m_token(token), : Task(nullptr, false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result())
m_type(type), {}
m_modFile(modFile),
m_result(new Result())
{
}
void LocalModParseTask::processAsZip() void LocalModParseTask::processAsZip()
{ {
@ -354,10 +297,8 @@ void LocalModParseTask::processAsZip()
QuaZipFile file(&zip); QuaZipFile file(&zip);
if (zip.setCurrentFile("META-INF/mods.toml")) if (zip.setCurrentFile("META-INF/mods.toml")) {
{ if (!file.open(QIODevice::ReadOnly)) {
if (!file.open(QIODevice::ReadOnly))
{
zip.close(); zip.close();
return; return;
} }
@ -366,12 +307,9 @@ void LocalModParseTask::processAsZip()
file.close(); file.close();
// to replace ${file.jarVersion} with the actual version, as needed // 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")) {
if (zip.setCurrentFile("META-INF/MANIFEST.MF")) if (!file.open(QIODevice::ReadOnly)) {
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close(); zip.close();
return; return;
} }
@ -379,10 +317,8 @@ void LocalModParseTask::processAsZip()
// quick and dirty line-by-line parser // quick and dirty line-by-line parser
auto manifestLines = file.readAll().split('\n'); auto manifestLines = file.readAll().split('\n');
QString manifestVersion = ""; QString manifestVersion = "";
for (auto &line : manifestLines) for (auto& line : manifestLines) {
{ if (QString(line).startsWith("Implementation-Version: ")) {
if (QString(line).startsWith("Implementation-Version: "))
{
manifestVersion = QString(line).remove("Implementation-Version: "); manifestVersion = QString(line).remove("Implementation-Version: ");
break; break;
} }
@ -390,12 +326,11 @@ void LocalModParseTask::processAsZip()
// some mods use ${projectversion} in their build.gradle, causing this mess to show up in MANIFEST.MF // some mods use ${projectversion} in their build.gradle, causing this mess to show up in MANIFEST.MF
// also keep with forge's behavior of setting the version to "NONE" if none is found // also keep with forge's behavior of setting the version to "NONE" if none is found
if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "") if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "") {
{
manifestVersion = "NONE"; manifestVersion = "NONE";
} }
m_result->details->version = manifestVersion; m_result->details.version = manifestVersion;
file.close(); file.close();
} }
@ -403,11 +338,8 @@ void LocalModParseTask::processAsZip()
zip.close(); zip.close();
return; return;
} } else if (zip.setCurrentFile("mcmod.info")) {
else if (zip.setCurrentFile("mcmod.info")) if (!file.open(QIODevice::ReadOnly)) {
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close(); zip.close();
return; return;
} }
@ -416,11 +348,8 @@ void LocalModParseTask::processAsZip()
file.close(); file.close();
zip.close(); zip.close();
return; return;
} } else if (zip.setCurrentFile("quilt.mod.json")) {
else if (zip.setCurrentFile("quilt.mod.json")) if (!file.open(QIODevice::ReadOnly)) {
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close(); zip.close();
return; return;
} }
@ -429,11 +358,8 @@ void LocalModParseTask::processAsZip()
file.close(); file.close();
zip.close(); zip.close();
return; return;
} } else if (zip.setCurrentFile("fabric.mod.json")) {
else if (zip.setCurrentFile("fabric.mod.json")) if (!file.open(QIODevice::ReadOnly)) {
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close(); zip.close();
return; return;
} }
@ -442,11 +368,8 @@ void LocalModParseTask::processAsZip()
file.close(); file.close();
zip.close(); zip.close();
return; return;
} } else if (zip.setCurrentFile("forgeversion.properties")) {
else if (zip.setCurrentFile("forgeversion.properties")) if (!file.open(QIODevice::ReadOnly)) {
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close(); zip.close();
return; return;
} }
@ -463,8 +386,7 @@ void LocalModParseTask::processAsZip()
void LocalModParseTask::processAsFolder() void LocalModParseTask::processAsFolder()
{ {
QFileInfo mcmod_info(FS::PathCombine(m_modFile.filePath(), "mcmod.info")); QFileInfo mcmod_info(FS::PathCombine(m_modFile.filePath(), "mcmod.info"));
if (mcmod_info.isFile()) if (mcmod_info.isFile()) {
{
QFile mcmod(mcmod_info.filePath()); QFile mcmod(mcmod_info.filePath());
if (!mcmod.open(QIODevice::ReadOnly)) if (!mcmod.open(QIODevice::ReadOnly))
return; return;
@ -483,10 +405,8 @@ void LocalModParseTask::processAsLitemod()
QuaZipFile file(&zip); QuaZipFile file(&zip);
if (zip.setCurrentFile("litemod.json")) if (zip.setCurrentFile("litemod.json")) {
{ if (!file.open(QIODevice::ReadOnly)) {
if (!file.open(QIODevice::ReadOnly))
{
zip.close(); zip.close();
return; return;
} }
@ -497,21 +417,30 @@ void LocalModParseTask::processAsLitemod()
zip.close(); zip.close();
} }
void LocalModParseTask::run() bool LocalModParseTask::abort()
{ {
switch(m_type) m_aborted.store(true);
return true;
}
void LocalModParseTask::executeTask()
{ {
case Mod::MOD_ZIPFILE: switch (m_type) {
case ResourceType::ZIPFILE:
processAsZip(); processAsZip();
break; break;
case Mod::MOD_FOLDER: case ResourceType::FOLDER:
processAsFolder(); processAsFolder();
break; break;
case Mod::MOD_LITEMOD: case ResourceType::LITEMOD:
processAsLitemod(); processAsLitemod();
break; break;
default: default:
break; break;
} }
emit finished(m_token);
if (m_aborted)
emit finished();
else
emitSucceeded();
} }

View File

@ -2,29 +2,31 @@
#include <QDebug> #include <QDebug>
#include <QObject> #include <QObject>
#include <QRunnable>
#include "minecraft/mod/Mod.h" #include "minecraft/mod/Mod.h"
#include "minecraft/mod/ModDetails.h" #include "minecraft/mod/ModDetails.h"
class LocalModParseTask : public QObject, public QRunnable #include "tasks/Task.h"
class LocalModParseTask : public Task
{ {
Q_OBJECT Q_OBJECT
public: public:
struct Result { struct Result {
QString id; ModDetails details;
std::shared_ptr<ModDetails> details;
}; };
using ResultPtr = std::shared_ptr<Result>; using ResultPtr = std::shared_ptr<Result>;
ResultPtr result() const { ResultPtr result() const {
return m_result; return m_result;
} }
LocalModParseTask(int token, Mod::ModType type, const QFileInfo & modFile); [[nodiscard]] bool canAbort() const override { return true; }
void run(); bool abort() override;
signals: LocalModParseTask(int token, ResourceType type, const QFileInfo & modFile);
void finished(int token); void executeTask() override;
[[nodiscard]] int token() const { return m_token; }
private: private:
void processAsZip(); void processAsZip();
@ -33,7 +35,9 @@ private:
private: private:
int m_token; int m_token;
Mod::ModType m_type; ResourceType m_type;
QFileInfo m_modFile; QFileInfo m_modFile;
ResultPtr m_result; ResultPtr m_result;
std::atomic<bool> m_aborted = false;
}; };

Some files were not shown because too many files have changed in this diff Show More