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:
label: Version of PolyMC
description: The version of PolyMC used in the bug report.
placeholder: PolyMC 1.3.2
placeholder: PolyMC 1.4.1
validations:
required: true
- type: textarea
attributes:
label: Version of Qt
description: The version of Qt used in the bug report. You can find it in Help -> About PolyMC -> About Qt.
placeholder: Qt 6.3.0
validations:
required: true
- type: textarea

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_version: '6.2.4'
qt_modules: 'qt5compat qtimageformats'
qt_path: /home/runner/work/PolyMC/Qt
- os: windows-2022
name: "Windows-Legacy"
@ -40,12 +39,20 @@ jobs:
qt_ver: 6
- os: macos-12
macosx_deployment_target: 10.14
name: macOS
macosx_deployment_target: 10.15
qt_ver: 6
qt_host: mac
qt_version: '6.3.1'
qt_version: '6.3.0'
qt_modules: 'qt5compat qtimageformats'
qt_path: /Users/runner/work/PolyMC/Qt
- 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 }}
@ -66,6 +73,14 @@ jobs:
with:
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'
if: runner.os == 'Windows'
uses: msys2/setup-msys2@v2
@ -141,24 +156,16 @@ jobs:
run: |
sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5
- name: Cache Qt (macOS and AppImage)
id: cache-qt
if: matrix.qt_ver == 6 && runner.os != 'Windows'
uses: actions/cache@v3
with:
path: '${{ matrix.qt_path }}/${{ matrix.qt_version }}'
key: ${{ matrix.qt_host }}-${{ matrix.qt_version }}-"${{ matrix.qt_modules }}"-qt_cache
- name: Install Qt (macOS and AppImage)
if: matrix.qt_ver == 6 && runner.os != 'Windows'
uses: jurplel/install-qt-action@v2
if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS'
uses: jurplel/install-qt-action@v3
with:
version: ${{ matrix.qt_version }}
host: ${{ matrix.qt_host }}
target: 'desktop'
modules: ${{ matrix.qt_modules }}
cached: ${{ steps.cache-qt.outputs.cache-hit }}
aqtversion: ==2.1.*
cache: true
cache-key-prefix: ${{ matrix.qt_host }}-${{ matrix.qt_version }}-"${{ matrix.qt_modules }}"-qt_cache
- name: Prepare AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5
@ -174,9 +181,14 @@ jobs:
##
- name: Configure CMake (macOS)
if: runner.os == 'macOS'
if: runner.os == 'macOS' && matrix.qt_ver == 6
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)
if: runner.os == 'Windows'
@ -219,6 +231,14 @@ jobs:
run: |
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
##
@ -234,7 +254,7 @@ jobs:
tar -czf ../PolyMC.tar.gz *
- name: Make Sparkle signature (macOS)
if: runner.os == 'macOS'
if: matrix.name == 'macOS'
run: |
if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then
brew install openssl@3
@ -259,10 +279,8 @@ jobs:
cmake --install ${{ env.BUILD_DIR }}
cd ${{ env.INSTALL_DIR }}
if [ "${{ matrix.msystem }}" == "mingw32" ]; then
if [ "${{ matrix.qt_ver }}" == "5" ]; then
cp /mingw32/bin/libcrypto-1_1.dll /mingw32/bin/libssl-1_1.dll ./
elif [ "${{ matrix.msystem }}" == "mingw64" ]; then
cp /mingw64/bin/libcrypto-1_1-x64.dll /mingw64/bin/libssl-1_1-x64.dll ./
fi
- name: Package (Windows, portable)
@ -314,6 +332,9 @@ jobs:
cp -r ${{ github.workspace }}/JREs/jre17/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk
cp -r /home/runner/work/PolyMC/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}//usr/lib/
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server"
@ -332,7 +353,7 @@ jobs:
if: runner.os == 'macOS'
uses: actions/upload-artifact@v3
with:
name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}
name: PolyMC-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
path: PolyMC.tar.gz
- name: Upload binary zip (Windows)

View File

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

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*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz
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
tar -czf PolyMC-${{ env.VERSION }}.tar.gz PolyMC-${{ env.VERSION }}
@ -80,4 +81,5 @@ jobs:
PolyMC-Windows-Portable-${{ env.VERSION }}.zip
PolyMC-Windows-Setup-${{ env.VERSION }}.exe
PolyMC-macOS-${{ env.VERSION }}.tar.gz
PolyMC-macOS-Legacy-${{ env.VERSION }}.tar.gz
PolyMC-${{ env.VERSION }}.tar.gz

View File

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

8
.gitmodules vendored
View File

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

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(CMAKE_CXX_STANDARD_REQUIRED true)
set(CMAKE_C_STANDARD_REQUIRED true)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_C_STANDARD 11)
include(GenerateExportHeader)
set(CMAKE_CXX_FLAGS "-Wall -pedantic -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}")
if(UNIX AND APPLE)
set(CMAKE_CXX_FLAGS "-stdlib=libc++ ${CMAKE_CXX_FLAGS}")
endif()
set(CMAKE_CXX_FLAGS "-Wall -pedantic -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}")
# Fix build with Qt 5.13
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y")
@ -67,7 +64,7 @@ find_package(ECM REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH "${ECM_MODULE_PATH};${CMAKE_MODULE_PATH}")
include(CTest)
include(ECMAddTests)
if (BUILD_TESTING)
if(BUILD_TESTING)
enable_testing()
endif()
@ -79,12 +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 version numbers ########
set(Launcher_VERSION_MAJOR 1)
set(Launcher_VERSION_MINOR 4)
set(Launcher_VERSION_HOTFIX 0)
set(Launcher_VERSION_MAJOR 5)
set(Launcher_VERSION_MINOR 0)
# Build number
set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.")
set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.0.0")
set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},0,0")
# Build platform.
set(Launcher_BUILD_PLATFORM "" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.")
@ -132,15 +129,8 @@ message(STATUS "Git commit: ${Launcher_GIT_COMMIT}")
message(STATUS "Git tag: ${Launcher_GIT_TAG}")
message(STATUS "Git refspec: ${Launcher_GIT_REFSPEC}")
set(Launcher_RELEASE_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}")
set(Launcher_RELEASE_VERSION_NAME4 "${Launcher_RELEASE_VERSION_NAME}.0")
set(Launcher_RELEASE_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},${Launcher_VERSION_HOTFIX},0")
string(TIMESTAMP TODAY "%Y-%m-%d")
set(Launcher_RELEASE_TIMESTAMP "${TODAY}")
#### Custom target to just print the version.
add_custom_target(version echo "Version: ${Launcher_RELEASE_VERSION_NAME}")
add_custom_target(tcversion echo "\\#\\#teamcity[setParameter name=\\'env.LAUNCHER_VERSION\\' value=\\'${Launcher_RELEASE_VERSION_NAME}\\']")
set(Launcher_BUILD_TIMESTAMP "${TODAY}")
################################ 3rd Party Libs ################################
@ -188,6 +178,14 @@ if (Qt5_POSITION_INDEPENDENT_CODE)
SET(CMAKE_POSITION_INDEPENDENT_CODE ON)
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 #######################################
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/launcher) # java based launcher part for Minecraft
add_subdirectory(libraries/javacheck) # java compatibility checker
add_subdirectory(libraries/xz-embedded) # xz compression
if (FORCE_BUNDLED_QUAZIP)
message(STATUS "Using bundled QuaZip")
set(BUILD_SHARED_LIBS 0) # link statically to avoid conflicts.
@ -307,16 +304,30 @@ else()
endif()
add_subdirectory(libraries/rainbow) # Qt extension for colors
add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions
add_subdirectory(libraries/classparser) # class parser library
add_subdirectory(libraries/optional-bare)
add_subdirectory(libraries/tomlc99) # toml parser
if(NOT tomlplusplus_FOUND)
message(STATUS "Using bundled tomlplusplus")
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/gamemode)
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 ###############################
add_subdirectory(buildconfig)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
# NOTE: this must always be last to appease the CMake deity of quirky install command evaluation order.
add_subdirectory(launcher)

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.
In general, in order of importance:
- Make sure your IDE is not messing up line endings or whitespace and avoid using linters.
- Prefer readability over dogma.
- Keep to the existing formatting.
@ -26,37 +27,37 @@ Signed-off-by: Author name <Author email>
By signing off your work, you agree to the terms below:
Developer's Certificate of Origin 1.1
```
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:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
These terms will be enforced once you create a pull request, and you will be informed automatically if any of your commits aren't signed-off by you.
As a bonus, you can also [cryptographically sign your commits][gh-signing-commits] and enable [vigilant mode][gh-vigilant-mode] on GitHub.
[gh-signing-commits]: https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits
[gh-vigilant-mode]: https://docs.github.com/en/authentication/managing-commit-signature-verification/displaying-verification-statuses-for-all-of-your-commits

View File

@ -1,4 +1,4 @@
# PolyMC
## PolyMC
PolyMC - Minecraft Launcher
Copyright (C) 2021-2022 PolyMC Contributors
@ -32,36 +32,56 @@
See the License for the specific language governing permissions and
limitations under the License.
# MinGW runtime (Windows)
## MinGW-w64 runtime (Windows)
Copyright (c) 2012 MinGW.org project
Copyright (c) 2009, 2010, 2011, 2012, 2013 by the mingw-w64 project
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
This license has been certified as open source. It has also been designated
as GPL compatible by the Free Software Foundation (FSF).
The above copyright notice, this permission notice and the below disclaimer
shall be included in all copies or substantial portions of the Software.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
1. Redistributions in source code must retain the accompanying copyright
notice, this list of conditions, and the following disclaimer.
2. Redistributions in binary form must reproduce the accompanying
copyright notice, this list of conditions, and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
3. Names of the copyright holders must not be used to endorse or promote
products derived from this software without prior written permission
from the copyright holders.
4. The right to distribute this software or to use it for any purpose does
not give you the right to use Servicemarks (sm) or Trademarks (tm) of
the copyright holders. Use of them is covered by separate agreement
with the copyright holders.
5. If any files are modified, you must cause the modified files to carry
prominent notices stating that you changed the files and the date of
any change.
# 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.
Contact: https://www.qt.io/licensing
Licensed under LGPL v3
# libnbt++
## libnbt++
libnbt++ - A library for the Minecraft Named Binary Tag format.
Copyright (C) 2013, 2015 ljfa-ag
@ -79,7 +99,7 @@
You should have received a copy of the GNU Lesser General Public License
along with libnbt++. If not, see <http://www.gnu.org/licenses/>.
# rainbow (KGuiAddons)
## rainbow (KGuiAddons)
Copyright (C) 2007 Matthew Woehlke <mw_triad@users.sourceforge.net>
Copyright (C) 2007 Olaf Schmidt <ojschmidt@kde.org>
@ -102,7 +122,7 @@
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
# Hoedown
## Hoedown
Copyright (c) 2008, Natacha Porté
Copyright (c) 2011, Vicent Martí
@ -120,7 +140,7 @@
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# Batch icon set
## Batch icon set
You are free to use Batch (the "icon set") or any part thereof (the "icons")
in any personal, open-source or commercial work without obligation of payment
@ -136,7 +156,7 @@
PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THE USE OF THE ICONS,
EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
# Material Design Icons
## Material Design Icons
Copyright (c) 2014, Austin Andrews (http://materialdesignicons.com/),
with Reserved Font Name Material Design Icons.
@ -147,7 +167,7 @@
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
# Quazip
## Quazip
Copyright (C) 2005-2021 Sergey A. Tachenov
@ -171,7 +191,7 @@
See COPYING file for the full LGPL text.
# xz-minidec
## xz-minidec
XZ decompressor
@ -181,7 +201,7 @@
This file has been put into the public domain.
You can do whatever you want with this file.
# ColumnResizer
## ColumnResizer
Copyright (c) 2011-2016 Aurélien Gâteau and contributors.
@ -217,7 +237,7 @@
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# launcher (`libraries/launcher`)
## launcher (`libraries/launcher`)
PolyMC - Minecraft Launcher
Copyright (C) 2021-2022 PolyMC Contributors
@ -268,7 +288,7 @@
See the License for the specific language governing permissions and
limitations under the License.
# lionshead
## lionshead
Code has been taken from https://github.com/natefoo/lionshead and loosely
translated to C++ laced with Qt.
@ -295,60 +315,26 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# optional-bare
Code from https://github.com/martinmoene/optional-bare/
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
# tomlc99
## tomlplusplus
MIT License
Copyright (c) 2017 CK Tan
https://github.com/cktan/tomlc99
Copyright (c) Mark Gillard <mark.gillard@outlook.com.au>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# O2 (Katabasis fork)
## O2 (Katabasis fork)
Copyright (c) 2012, Akos Polster
All rights reserved.
@ -373,3 +359,32 @@
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## Gamemode
Copyright (c) 2017-2022, Feral Interactive
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Feral Interactive nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@ -167,11 +167,12 @@ The translation effort for SneedMC is hosted on [Weblate](https://hosted.weblate
## 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
SNEED
## Sponsors
Money is not sneedful

View File

@ -55,18 +55,9 @@ Config::Config()
// Version information
VERSION_MAJOR = @Launcher_VERSION_MAJOR@;
VERSION_MINOR = @Launcher_VERSION_MINOR@;
VERSION_HOTFIX = @Launcher_VERSION_HOTFIX@;
VERSION_BUILD = @Launcher_VERSION_BUILD@;
BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@";
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;
}
BUILD_DATE = "@Launcher_BUILD_TIMESTAMP@";
GIT_COMMIT = "@Launcher_GIT_COMMIT@";
GIT_TAG = "@Launcher_GIT_TAG@";
@ -94,7 +85,6 @@ Config::Config()
VERSION_CHANNEL = "unknown";
}
VERSION_STR = "@Launcher_VERSION_STRING@";
NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@";
NEWS_OPEN_URL = "@Launcher_NEWS_OPEN_URL@";
HELP_URL = "@Launcher_HELP_URL@";
@ -112,7 +102,7 @@ Config::Config()
QString Config::versionString() const
{
return QString("%1.%2.%3").arg(VERSION_MAJOR).arg(VERSION_MINOR).arg(VERSION_HOTFIX);
return QString("%1.%2").arg(VERSION_MAJOR).arg(VERSION_MINOR);
}
QString Config::printableVersionString() const
@ -124,11 +114,5 @@ QString Config::printableVersionString() const
{
vstr += "-" + VERSION_CHANNEL;
}
// if a build number is set, also add it to the end
if(VERSION_BUILD >= 0)
{
vstr += "+build." + QString::number(VERSION_BUILD);
}
return vstr;
}

View File

@ -55,14 +55,13 @@ class Config {
int VERSION_MAJOR;
/// The minor version number.
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".
QString BUILD_PLATFORM;
/// A string containing the build timestamp
QString BUILD_DATE;
/// User-Agent to use.
QString USER_AGENT;
@ -80,9 +79,6 @@ class Config {
/// The git refspec of this build
QString GIT_REFSPEC;
/// This is printed on start to standard output
QString VERSION_STR;
/**
* This is used to fetch the news RSS feed.
* It defaults in CMakeLists.txt to "https://multimc.org/rss.xml"

View File

@ -52,7 +52,24 @@
"inputs": {
"flake-compat": "flake-compat",
"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";
flake-compat = { url = "github:edolstra/flake-compat"; 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
# User-friendly version number.
version = builtins.substring 0 8 self.lastModifiedDate;
@ -22,8 +23,8 @@
pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system});
packagesFn = pkgs: rec {
polymc = pkgs.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus; };
polymc-qt6 = pkgs.qt6Packages.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 tomlplusplus; };
};
in
{

View File

@ -60,6 +60,10 @@
#include "ui/themes/BrightTheme.h"
#include "ui/themes/CustomTheme.h"
#ifdef Q_OS_WIN
#include "ui/WinDarkmode.h"
#endif
#include "ui/setupwizard/SetupWizard.h"
#include "ui/setupwizard/LanguageWizardPage.h"
#include "ui/setupwizard/JavaWizardPage.h"
@ -74,6 +78,7 @@
#include <iostream>
#include <QAccessible>
#include <QCommandLineParser>
#include <QDir>
#include <QFileInfo>
#include <QNetworkAccessManager>
@ -104,13 +109,17 @@
#include "translations/TranslationsModel.h"
#include "meta/Index.h"
#include <Commandline.h>
#include <FileSystem.h>
#include <DesktopServices.h>
#include <LocalPeer.h>
#include <sys.h>
#ifdef Q_OS_LINUX
#include <dlfcn.h>
#include "gamemode_client.h"
#endif
#if defined Q_OS_WIN32
#ifndef WIN32_LEAN_AND_MEAN
@ -125,12 +134,6 @@
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 {
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);
// Commandline parsing
QHash<QString, QVariant> args;
{
Parser parser(FlagStyle::GNU, ArgumentStyle::SpaceAndEquals);
QCommandLineParser parser;
parser.setApplicationDescription(BuildConfig.LAUNCHER_NAME);
// --help
parser.addSwitch("help");
parser.addShortOpt("help", 'h');
parser.addDocumentation("help", "Display this help and exit.");
// --version
parser.addSwitch("version");
parser.addShortOpt("version", 'V');
parser.addDocumentation("version", "Display program version and exit.");
// --dir
parser.addOption("dir");
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)");
parser.addOptions({
{{"d", "dir"}, "Use a custom path as application root (use '.' for current directory)", "directory"},
{{"l", "launch"}, "Launch the specified instance (by instance ID)", "instance"},
{{"s", "server"}, "Join the specified server on launch (only valid in combination with --launch)", "address"},
{{"a", "profile"}, "Use the account specified by its profile name (only valid in combination with --launch)", "profile"},
{"alive", "Write a small '" + liveCheckFile + "' file after the launcher starts"},
{{"I", "import"}, "Import instance from specified zip (local path or URL)", "file"}
});
parser.addHelpOption();
parser.addVersionOption();
// parse the 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;
}
parser.process(arguments());
// display help and exit
if (args["help"].toBool())
{
std::cout << qPrintable(parser.compileHelp(arguments()[0]));
m_status = Application::Succeeded;
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();
m_instanceIdToLaunch = parser.value("launch");
m_serverToJoin = parser.value("server");
m_profileToUse = parser.value("profile");
m_liveCheck = parser.isSet("alive");
m_zipToImport = parser.value("import");
// error if --launch is missing with --server or --profile
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
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr
m_rootPath = foo.absolutePath();
#elif defined(Q_OS_WIN32)
@ -296,7 +246,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
QString adjustedBy;
QString dataPath;
// change folder
QString dirParam = args["dir"].toString();
QString dirParam = parser.value("dir");
if (!dirParam.isEmpty())
{
// 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(
"The launcher data folder could not be created.\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"
"(%1)\n"
"\n"
@ -361,9 +308,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
QString(
"The launcher data folder could not be opened.\n"
"\n"
#if defined(Q_OS_MAC)
MACOS_HINT
#endif
"Make sure you have the right permissions to the launcher data folder.\n"
"(%1)\n"
"\n"
@ -444,9 +388,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
QString(
"The launcher couldn't create a log file - the data folder is not writable.\n"
"\n"
#if defined(Q_OS_MAC)
MACOS_HINT
#endif
"Make sure you have write permissions to the data folder.\n"
"(%1)\n"
"\n"
@ -509,13 +450,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// Initialize application settings
{
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
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("ModDownloadGeometry", "");
// HACK: This code feels so stupid is there a less stupid way of doing this?
{
m_settings->registerSetting("PastebinURL", "");
@ -738,19 +674,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
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
{
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("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());
m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath());
m_metacache->addBase("FlameMods", QDir("cache/FlameMods").absolutePath());
m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath());
m_metacache->addBase("root", QDir::currentPath());
m_metacache->addBase("translations", QDir("translations").absolutePath());
@ -888,10 +812,13 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
qDebug() << "<> Application theme set.";
}
updateCapabilities();
if(createSetupWizard())
{
return;
}
performMainStartupAction();
}
@ -1000,7 +927,7 @@ void Application::performMainStartupAction()
qDebug() << " Launching with account" << m_profileToUse;
}
launch(inst, true, nullptr, serverToJoin, accountToUse);
launch(inst, true, false, nullptr, serverToJoin, accountToUse);
return;
}
}
@ -1104,6 +1031,7 @@ void Application::messageReceived(const QByteArray& message)
launch(
instance,
true,
false,
nullptr,
serverObject,
accountObject
@ -1158,6 +1086,15 @@ void Application::setApplicationTheme(const QString& name, bool initial)
{
auto & theme = (*themeIter).second;
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
{
@ -1195,6 +1132,7 @@ bool Application::openJsonEditor(const QString &filename)
bool Application::launch(
InstancePtr instance,
bool online,
bool demo,
BaseProfilerFactory *profiler,
MinecraftServerTargetPtr serverToJoin,
MinecraftAccountPtr accountToUse
@ -1214,6 +1152,7 @@ bool Application::launch(
controller.reset(new LaunchController());
controller->setInstance(instance);
controller->setOnline(online);
controller->setDemo(demo);
controller->setProfiler(profiler);
controller->setServerToJoin(serverToJoin);
controller->setAccountToUse(accountToUse);
@ -1227,6 +1166,9 @@ bool Application::launch(
}
connect(controller.get(), &LaunchController::succeeded, this, &Application::controllerSucceeded);
connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed);
connect(controller.get(), &LaunchController::aborted, this, [this] {
controllerFailed(tr("Aborted"));
});
addRunningInstance();
controller->start();
return true;
@ -1362,6 +1304,13 @@ MainWindow* Application::showMainWindow(bool minimized)
m_mainWindow = new MainWindow();
m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").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)
{
m_mainWindow->showMinimized();
@ -1372,10 +1321,6 @@ MainWindow* Application::showMainWindow(bool minimized)
}
m_mainWindow->checkInstancePathForProblems();
<<<<<<< HEAD
=======
connect(this, &Application::updateAllowedChanged, m_mainWindow, &MainWindow::updatesAllowedChanged);
>>>>>>> upstream/develop
connect(m_mainWindow, &MainWindow::isClosing, this, &Application::on_windowClose);
m_openWindows++;
}
@ -1517,14 +1462,30 @@ shared_qobject_ptr<Meta::Index> Application::metadataIndex()
return m_metadataIndex;
}
Application::Capabilities Application::currentCapabilities()
void Application::updateCapabilities()
{
Capabilities c;
m_capabilities = None;
if (!getMSAClientID().isEmpty())
c |= SupportsMSA;
m_capabilities |= SupportsMSA;
if (!getFlameAPIKey().isEmpty())
c |= SupportsFlame;
return c;
m_capabilities |= SupportsFlame;
#ifdef Q_OS_LINUX
if (gamemode_query_status() >= 0)
m_capabilities |= SupportsGameMode;
{
void *dummy = dlopen("libMangoHud_dlsym.so", RTLD_LAZY);
// try normal variant as well
if (dummy == NULL)
dummy = dlopen("libMangoHud.so", RTLD_LAZY);
if (dummy != NULL) {
dlclose(dummy);
m_capabilities |= SupportsMangoHud;
}
}
#endif
}
QString Application::getJarPath(QString jarFile)

View File

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

View File

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

View File

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

View File

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

View File

@ -92,412 +92,4 @@ QStringList splitArgs(QString args)
argv << current;
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
#include <exception>
#include <stdexcept>
#include <QString>
#include <QVariant>
#include <QHash>
#include <QStringList>
/**
@ -39,212 +34,4 @@ namespace Commandline
* @return a QStringList containing all arguments
*/
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,76 +35,100 @@
#include "FileSystem.h"
#include <QDir>
#include <QFile>
#include <QSaveFile>
#include <QFileInfo>
#include <QDebug>
#include <QUrl>
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QSaveFile>
#include <QStandardPaths>
#include <QTextStream>
#include <QUrl>
#if defined Q_OS_WIN32
#include <windows.h>
#include <string>
#include <sys/utime.h>
#include <winnls.h>
#include <shobjidl.h>
#include <objbase.h>
#include <objidl.h>
#include <shlguid.h>
#include <shlobj.h>
#include <objbase.h>
#include <objidl.h>
#include <shlguid.h>
#include <shlobj.h>
#include <shobjidl.h>
#include <sys/utime.h>
#include <windows.h>
#include <winnls.h>
#include <string>
#else
#include <utime.h>
#include <utime.h>
#endif
// 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 {
void ensureExists(const QDir &dir)
void ensureExists(const QDir& dir)
{
if (!QDir().mkpath(dir.absolutePath()))
{
throw FileSystemException("Unable to create folder " + dir.dirName() + " (" +
dir.absolutePath() + ")");
if (!QDir().mkpath(dir.absolutePath())) {
throw FileSystemException("Unable to create folder " + dir.dirName() + " (" + dir.absolutePath() + ")");
}
}
void write(const QString &filename, const QByteArray &data)
void write(const QString& filename, const QByteArray& data)
{
ensureExists(QFileInfo(filename).dir());
QSaveFile file(filename);
if (!file.open(QSaveFile::WriteOnly))
{
throw FileSystemException("Couldn't open " + filename + " for writing: " +
file.errorString());
if (!file.open(QSaveFile::WriteOnly)) {
throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString());
}
if (data.size() != file.write(data))
{
throw FileSystemException("Error writing data to " + filename + ": " +
file.errorString());
if (data.size() != file.write(data)) {
throw FileSystemException("Error writing data to " + filename + ": " + file.errorString());
}
if (!file.commit())
{
throw FileSystemException("Error while committing data to " + filename + ": " +
file.errorString());
if (!file.commit()) {
throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString());
}
}
QByteArray read(const QString &filename)
QByteArray read(const QString& filename)
{
QFile file(filename);
if (!file.open(QFile::ReadOnly))
{
throw FileSystemException("Unable to open " + filename + " for reading: " +
file.errorString());
if (!file.open(QFile::ReadOnly)) {
throw FileSystemException("Unable to open " + filename + " for reading: " + file.errorString());
}
const qint64 size = file.size();
QByteArray data(int(size), 0);
const qint64 ret = file.read(data.data(), size);
if (ret == -1 || ret != size)
{
throw FileSystemException("Error reading data from " + filename + ": " +
file.errorString());
if (ret == -1 || ret != size) {
throw FileSystemException("Error reading data from " + filename + ": " + file.errorString());
}
return data;
}
@ -138,149 +162,91 @@ bool ensureFolderPathExists(QString foldernamepath)
return success;
}
bool copy::operator()(const QString &offset)
bool copy::operator()(const QString& offset)
{
//NOTE always deep copy on windows. the alternatives are too messy.
#if defined Q_OS_WIN32
using copy_opts = fs::copy_options;
// NOTE always deep copy on windows. the alternatives are too messy.
#if defined Q_OS_WIN32
m_followSymlinks = true;
#endif
#endif
auto src = PathCombine(m_src.absolutePath(), offset);
auto dst = PathCombine(m_dst.absolutePath(), offset);
QFileInfo currentSrc(src);
if (!currentSrc.exists())
return false;
std::error_code err;
if(!m_followSymlinks && currentSrc.isSymLink())
{
qDebug() << "creating symlink" << src << " - " << dst;
if (!ensureFilePathExists(dst))
{
qWarning() << "Cannot create path!";
return false;
}
return QFile::link(currentSrc.symLinkTarget(), dst);
}
else if(currentSrc.isFile())
{
qDebug() << "copying file" << src << " - " << dst;
if (!ensureFilePathExists(dst))
{
qWarning() << "Cannot create path!";
return false;
}
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;
}
if(!operator()(inner_offset))
{
qWarning() << "Failed to copy" << inner_offset;
return false;
}
fs::copy_options opt = copy_opts::none;
// The default behavior is to follow symlinks
if (!m_followSymlinks)
opt |= copy_opts::copy_symlinks;
// We can't use copy_opts::recursive because we need to take into account the
// blacklisted paths, so we iterate over the source directory, and if there's no blacklist
// match, we copy the file.
QDir src_dir(src);
QDirIterator source_it(src, QDir::Filter::Files, QDirIterator::Subdirectories);
while (source_it.hasNext()) {
auto src_path = source_it.next();
auto relative_path = src_dir.relativeFilePath(src_path);
if (m_blacklist && m_blacklist->matches(relative_path))
continue;
auto dst_path = PathCombine(dst, relative_path);
ensureFilePathExists(dst_path);
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
{
qCritical() << "Copy ERROR: Unknown filesystem object:" << src;
return false;
}
return true;
return err.value() == 0;
}
bool deletePath(QString path)
{
bool OK = true;
QFileInfo finfo(path);
if(finfo.isFile()) {
return QFile::remove(path);
std::error_code err;
fs::remove_all(toStdString(path), err);
if (err) {
qWarning() << "Failed to remove files:" << QString::fromStdString(err.message());
}
QDir dir(path);
if (!dir.exists())
{
return OK;
}
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
// We do not trust Qt with reparse points, but do trust it with unix symlinks.
if(info.isSymLink())
{
OK &= QFile::remove(info.absoluteFilePath());
}
#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;
return err.value() == 0;
}
QString PathCombine(const QString & path1, const QString & path2)
bool trash(QString path, QString *pathInTrash = nullptr)
{
if(!path1.size())
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
return false;
#else
return QFile::moveToTrash(path, pathInTrash);
#endif
}
QString PathCombine(const QString& path1, const QString& path2)
{
if (!path1.size())
return path2;
if(!path2.size())
if (!path2.size())
return path1;
return QDir::cleanPath(path1 + QDir::separator() + path2);
}
QString PathCombine(const QString & path1, const QString & path2, const QString & path3)
QString PathCombine(const QString& path1, const QString& path2, const QString& path3)
{
return PathCombine(PathCombine(path1, path2), path3);
}
QString PathCombine(const QString & path1, const QString & path2, const QString & path3, const QString & path4)
QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4)
{
return PathCombine(PathCombine(path1, path2, path3), path4);
}
@ -292,17 +258,14 @@ QString AbsolutePath(QString path)
QString ResolveExecutable(QString path)
{
if (path.isEmpty())
{
if (path.isEmpty()) {
return QString();
}
if(!path.contains('/'))
{
if (!path.contains('/')) {
path = QStandardPaths::findExecutable(path);
}
QFileInfo pathInfo(path);
if(!pathInfo.exists() || !pathInfo.isExecutable())
{
if (!pathInfo.exists() || !pathInfo.isExecutable()) {
return QString();
}
return pathInfo.absoluteFilePath();
@ -322,12 +285,9 @@ QString NormalizePath(QString path)
QDir b(path);
QString newAbsolute = b.absolutePath();
if (newAbsolute.startsWith(currentAbsolute))
{
if (newAbsolute.startsWith(currentAbsolute)) {
return a.relativeFilePath(newAbsolute);
}
else
{
} else {
return newAbsolute;
}
}
@ -336,10 +296,8 @@ QString badFilenameChars = "\"\\/?<>:;*|!+\r\n";
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{
for (int i = 0; i < string.length(); i++)
{
if (badFilenameChars.contains(string[i]))
{
for (int i = 0; i < string.length(); i++) {
if (badFilenameChars.contains(string[i])) {
string[i] = replaceWith;
}
}
@ -351,15 +309,11 @@ QString DirNameFromString(QString string, QString inDir)
int num = 0;
QString baseName = RemoveInvalidFilenameChars(string, '-');
QString dirName;
do
{
if(num == 0)
{
do {
if (num == 0) {
dirName = baseName;
}
else
{
dirName = baseName + QString::number(num);;
} else {
dirName = baseName + "(" + QString::number(num) + ")";
}
// If it's over 9000
@ -378,63 +332,13 @@ bool checkProblemticPathJava(QDir folder)
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()
{
return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
}
// Cross-platform Shortcut creation
bool createShortCut(QString location, QString dest, QStringList args, QString name,
QString icon)
bool createShortCut(QString location, QString dest, QStringList args, QString name, QString icon)
{
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
location = PathCombine(location, name + ".desktop");
@ -459,8 +363,7 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na
stream.flush();
f.close();
f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup |
QFileDevice::ExeOther);
f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther);
return true;
#elif defined Q_OS_WIN
@ -488,46 +391,24 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na
#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)
{
using copy_opts = fs::copy_options;
if (!FS::ensureFolderPathExists(overwritten_path))
return false;
QStringList paths_to_override;
QDir root_override (override_path);
for (auto file : listFolderPaths(root_override)) {
QString destination = file;
destination.replace(override_path, overwritten_path);
std::error_code err;
fs::copy_options opt = copy_opts::recursive | copy_opts::overwrite_existing;
qDebug() << QString("Applying override %1 in %2").arg(file, destination);
fs::copy(toStdString(override_path), toStdString(overwritten_path), opt, err);
if (QFile::exists(destination))
QFile::remove(destination);
if (!QFile::rename(file, destination)) {
qCritical() << QString("Failed to apply override from %1 to %2").arg(file, destination);
return false;
}
if (err) {
qCritical() << QString("Failed to apply override from %1 to %2").arg(override_path, overwritten_path);
qCritical() << "Reason:" << QString::fromStdString(err.message());
}
return true;
return err.value() == 0;
}
}

View File

@ -41,29 +41,27 @@
#include <QDir>
#include <QFlags>
namespace FS
{
namespace FS {
class FileSystemException : public ::Exception
{
public:
FileSystemException(const QString &message) : Exception(message) {}
class FileSystemException : public ::Exception {
public:
FileSystemException(const QString& message) : Exception(message) {}
};
/**
* write data to a file safely
*/
void write(const QString &filename, const QByteArray &data);
void write(const QString& filename, const QByteArray& data);
/**
* read data from a file safely\
*/
QByteArray read(const QString &filename);
QByteArray read(const QString& filename);
/**
* Update the last changed timestamp of an existing file
*/
bool updateTimestamp(const QString & filename);
bool updateTimestamp(const QString& filename);
/**
* Creates all the folders in a path for the specified path
@ -77,35 +75,31 @@ bool ensureFilePathExists(QString filenamepath);
*/
bool ensureFolderPathExists(QString filenamepath);
class copy
{
public:
copy(const QString & src, const QString & dst)
class copy {
public:
copy(const QString& src, const QString& dst)
{
m_src.setPath(src);
m_dst.setPath(dst);
}
copy & followSymlinks(const bool follow)
copy& followSymlinks(const bool follow)
{
m_followSymlinks = follow;
return *this;
}
copy & blacklist(const IPathMatcher * filter)
copy& blacklist(const IPathMatcher* filter)
{
m_blacklist = filter;
return *this;
}
bool operator()()
{
return operator()(QString());
}
bool operator()() { return operator()(QString()); }
private:
bool operator()(const QString &offset);
private:
bool operator()(const QString& offset);
private:
private:
bool m_followSymlinks = true;
const IPathMatcher * m_blacklist = nullptr;
const IPathMatcher* m_blacklist = nullptr;
QDir m_src;
QDir m_dst;
};
@ -115,9 +109,14 @@ private:
*/
bool deletePath(QString path);
QString PathCombine(const QString &path1, const QString &path2);
QString PathCombine(const QString &path1, const QString &path2, const QString &path3);
QString PathCombine(const QString &path1, const QString &path2, const QString &path3, const QString &path4);
/**
* Trash a folder / file
*/
bool trash(QString path, QString *pathInTrash);
QString PathCombine(const QString& path1, const QString& path2);
QString PathCombine(const QString& path1, const QString& path2, const QString& path3);
QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4);
QString AbsolutePath(QString path);

View File

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

View File

@ -1,40 +1,56 @@
#include "InstanceCreationTask.h"
#include "settings/INISettingsObject.h"
#include "FileSystem.h"
//FIXME: remove this
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include <QDebug>
#include <QFile>
InstanceCreationTask::InstanceCreationTask(BaseVersionPtr version)
{
m_version = version;
m_usingLoader = false;
}
InstanceCreationTask::InstanceCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loaderVersion)
{
m_version = version;
m_usingLoader = true;
m_loader = loader;
m_loaderVersion = loaderVersion;
}
InstanceCreationTask::InstanceCreationTask() = default;
void InstanceCreationTask::executeTask()
{
setStatus(tr("Creating instance from version %1").arg(m_version->name()));
{
auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg"));
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();
setAbortable(true);
if (updateInstance()) {
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
#include "tasks/Task.h"
#include "net/NetJob.h"
#include <QUrl>
#include "settings/SettingsObject.h"
#include "BaseVersion.h"
#include "InstanceTask.h"
class InstanceCreationTask : public InstanceTask
{
class InstanceCreationTask : public InstanceTask {
Q_OBJECT
public:
explicit InstanceCreationTask(BaseVersionPtr version);
explicit InstanceCreationTask(BaseVersionPtr version, QString loader, BaseVersionPtr loaderVersion);
public:
InstanceCreationTask();
virtual ~InstanceCreationTask() = default;
protected:
//! Entry point for tasks.
virtual void executeTask() override;
protected:
void executeTask() final override;
private: /* data */
BaseVersionPtr m_version;
bool m_usingLoader;
QString m_loader;
BaseVersionPtr m_loaderVersion;
/**
* Tries to update an already existing instance.
*
* This can be implemented by subclasses to provide a way of updating an already existing
* 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 <QtConcurrentRun>
#include "Application.h"
#include "BaseInstance.h"
#include "FileSystem.h"
#include "MMCZip.h"
#include "NullInstance.h"
#include "icons/IconList.h"
#include "icons/IconUtils.h"
#include "modplatform/technic/TechnicPackProcessor.h"
#include "modplatform/modrinth/ModrinthInstanceCreationTask.h"
#include "modplatform/flame/FlameInstanceCreationTask.h"
#include "settings/INISettingsObject.h"
// FIXME: this does not belong here, it's Minecraft/Flame specific
#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 <QtConcurrentRun>
#include <algorithm>
#include <quazip/quazipdir.h>
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
{
m_sourceUrl = sourceUrl;
@ -72,35 +63,41 @@ InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
bool InstanceImportTask::abort()
{
if (!canAbort())
return false;
if (m_filesNetJob)
m_filesNetJob->abort();
m_extractFuture.cancel();
return false;
return Task::abort();
}
void InstanceImportTask::executeTask()
{
if (m_sourceUrl.isLocalFile())
{
setAbortable(true);
if (m_sourceUrl.isLocalFile()) {
m_archivePath = m_sourceUrl.toLocalFile();
processZipPack();
}
else
{
} else {
setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
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);
entry->setStale(true);
m_archivePath = entry->getFullPath();
m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network());
m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
m_archivePath = entry->getFullPath();
auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
connect(job, &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged);
connect(job, &NetJob::failed, this, &InstanceImportTask::downloadFailed);
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted);
m_filesNetJob->start();
}
}
@ -119,7 +116,13 @@ void InstanceImportTask::downloadFailed(QString reason)
void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total)
{
setProgress(current / 2, total);
setProgress(current, total);
}
void InstanceImportTask::downloadAborted()
{
emitAborted();
m_filesNetJob.reset();
}
void InstanceImportTask::processZipPack()
@ -255,290 +258,31 @@ void InstanceImportTask::extractFinished()
void InstanceImportTask::extractAborted()
{
emitFailed(tr("Instance import has been aborted."));
return;
emitAborted();
}
void InstanceImportTask::processFlame()
{
const static QMap<QString,QString> forgemap = {
{"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));
}
}
auto* inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent);
QString forgeVersion;
QString fabricVersion;
// TODO: is Quilt relevant here?
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");
auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
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();
}
);
connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) {
m_filesNetJob.reset();
emitFailed(reason);
});
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
setProgress(current, total);
});
setStatus(tr("Downloading mods..."));
m_filesNetJob->start();
} else {
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);
auto path = FS::PathCombine(m_stagingPath, relpath);
switch (result.type) {
case Flame::File::Type::Folder: {
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
// fall-through intentional, we treat these as plain old mods and dump them wherever.
}
case Flame::File::Type::SingleFile:
case Flame::File::Type::Mod: {
if (!result.url.isEmpty()) {
qDebug() << "Will download" << result.url << "to" << path;
auto dl = Net::Download::makeFile(result.url, path);
m_filesNetJob->addNetAction(dl);
}
break;
}
case Flame::File::Type::Modpack:
logWarning(
tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(
relpath));
break;
case Flame::File::Type::Cmod2:
case Flame::File::Type::Ctoc:
case Flame::File::Type::Unknown:
logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
break;
}
}
m_modIdResolver.reset();
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() {
m_filesNetJob.reset();
emitSucceeded();
}
);
connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) {
m_filesNetJob.reset();
emitFailed(reason);
});
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
setProgress(current, total);
});
setStatus(tr("Downloading mods..."));
m_filesNetJob->start();
}
}
);
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason)
{
m_modIdResolver.reset();
emitFailed(tr("Unable to resolve mod IDs:\n") + reason);
inst_creation_task->setName(*this);
inst_creation_task->setIcon(m_instIcon);
inst_creation_task->setGroup(m_instGroup);
connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] {
setOverride(inst_creation_task->shouldOverride());
emitSucceeded();
});
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();
connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress);
connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);
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();
}
void InstanceImportTask::processTechnic()
@ -546,7 +290,7 @@ void InstanceImportTask::processTechnic()
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded);
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()
@ -560,7 +304,7 @@ void InstanceImportTask::processMultiMC()
instance.resetTimePlayed();
// 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 (m_instIcon != "default") {
@ -581,198 +325,26 @@ void InstanceImportTask::processMultiMC()
emitSucceeded();
}
// https://docs.modrinth.com/docs/modpacks/format_definition/
void InstanceImportTask::processModrinth()
{
std::vector<Modrinth::File> files;
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* inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, m_sourceUrl.toString());
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
bool had_optional = false;
for (auto modInfo : jsonFiles) {
Modrinth::File file;
file.path = Json::requireString(modInfo, "path");
auto env = Json::ensureObject(modInfo, "env");
// 'env' field is optional
if (!env.isEmpty()) {
QString support = Json::ensureString(env, "client", "unsupported");
if (support == "unsupported") {
continue;
} else if (support == "optional") {
// TODO: Make a review dialog for choosing which ones the user wants!
if (!had_optional) {
had_optional = true;
auto info = CustomMessageBox::selectable(
m_parent, tr("Optional mod detected!"),
tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"),
QMessageBox::Information);
info->exec();
}
if (file.path.endsWith(".jar"))
file.path += ".disabled";
}
}
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;
}
inst_creation_task->setName(*this);
inst_creation_task->setIcon(m_instIcon);
inst_creation_task->setGroup(m_instGroup);
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();
}
);
connect(m_filesNetJob.get(), &NetJob::failed, [&](const QString &reason)
{
m_filesNetJob.reset();
emitFailed(reason);
connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] {
setOverride(inst_creation_task->shouldOverride());
emitSucceeded();
});
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total)
{
setProgress(current, total);
});
setStatus(tr("Downloading mods..."));
m_filesNetJob->start();
connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress);
connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);
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 "modplatform/flame/PackManifest.h"
#include <nonstd/optional>
#include <optional>
class QuaZip;
namespace Flame
@ -58,7 +58,6 @@ class InstanceImportTask : public InstanceTask
public:
explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr);
bool canAbort() const override { return true; }
bool abort() override;
const QVector<Flame::File> &getBlockedFiles() const
{
@ -80,6 +79,7 @@ private slots:
void downloadSucceeded();
void downloadFailed(QString reason);
void downloadProgressChanged(qint64 current, qint64 total);
void downloadAborted();
void extractFinished();
void extractAborted();
@ -90,8 +90,8 @@ private: /* data */
QString m_archivePath;
bool m_downloadRequired = false;
std::unique_ptr<QuaZip> m_packZip;
QFuture<nonstd::optional<QStringList>> m_extractFuture;
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
QFuture<std::optional<QStringList>> m_extractFuture;
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
QVector<Flame::File> m_blockedMods;
enum class ModpackType{
Unknown,

View File

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

View File

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

View File

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

View File

@ -1,9 +1,52 @@
#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
#include "tasks/Task.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
public:
explicit InstanceTask();
virtual ~InstanceTask();
public:
InstanceTask();
~InstanceTask() override = default;
void setParentSettings(SettingsObjectPtr settings)
{
m_globalSettings = settings;
}
void setParentSettings(SettingsObjectPtr settings) { m_globalSettings = settings; }
void setStagingPath(const QString &stagingPath)
{
m_stagingPath = stagingPath;
}
void setStagingPath(const QString& stagingPath) { m_stagingPath = stagingPath; }
void setName(const QString &name)
{
m_instName = name;
}
QString name() const
{
return m_instName;
}
void setIcon(const QString& icon) { m_instIcon = icon; }
void setIcon(const QString &icon)
{
m_instIcon = icon;
}
void setGroup(const QString& group) { m_instGroup = group; }
QString group() const { return m_instGroup; }
void setGroup(const QString &group)
{
m_instGroup = group;
}
QString group() const
{
return m_instGroup;
}
bool shouldOverride() const { return m_override_existing; }
protected: /* data */
protected:
void setOverride(bool override) { m_override_existing = override; }
protected: /* data */
SettingsObjectPtr m_globalSettings;
QString m_instName;
QString m_instIcon;
QString m_instGroup;
QString m_stagingPath;
bool m_override_existing = false;
};

View File

@ -93,8 +93,8 @@ void LaunchController::decideAccount()
auto reply = CustomMessageBox::selectable(
m_parentWidget,
tr("No Accounts"),
tr("In order to play Minecraft, you must have at least one Mojang or Microsoft "
"account logged in. "
tr("In order to play Minecraft, you must have at least one Microsoft or Mojang "
"account logged in. Mojang accounts can only be used offline. "
"Would you like to open the account manager to add an account now?"),
QMessageBox::Information,
QMessageBox::Yes | QMessageBox::No
@ -145,18 +145,29 @@ void LaunchController::login() {
return;
}
// we try empty password first :)
QString password;
// we loop until the user succeeds in logging in or gives up
bool tryagain = true;
// the failure. the default failure.
const QString needLoginAgain = tr("Your account is currently not logged in. Please enter your password to log in again. <br /> <br /> This could be caused by a password change.");
QString failReason = needLoginAgain;
unsigned int tries = 0;
while (tryagain)
{
if (tries > 0 && tries % 3 == 0) {
auto result = QMessageBox::question(
m_parentWidget,
tr("Continue launch?"),
tr("It looks like we couldn't launch after %1 tries. Do you want to continue trying?")
.arg(tries)
);
if (result == QMessageBox::No) {
emitAborted();
return;
}
}
tries++;
m_session = std::make_shared<AuthSession>();
m_session->wants_online = m_online;
m_session->demo = m_demo;
m_accountToUse->fillSession(m_session);
// Launch immediately in true offline mode
@ -174,12 +185,18 @@ void LaunchController::login() {
if(!m_session->wants_online) {
// we ask the user for a player name
bool ok = false;
QString message = tr("Choose your offline mode player name.");
if(m_session->demo) {
message = tr("Choose your demo mode player name.");
}
QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString();
QString usedname = lastOfflinePlayerName.isEmpty() ? m_session->player_name : lastOfflinePlayerName;
QString name = QInputDialog::getText(
m_parentWidget,
tr("Player name"),
tr("Choose your offline mode player name."),
message,
QLineEdit::Normal,
usedname,
&ok
@ -359,7 +376,7 @@ void LaunchController::launchInstance()
}
m_launcher->prependStep(new TextPrint(m_launcher.get(), resolved_servers, MessageLevel::Launcher));
} 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));

View File

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

View File

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

View File

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

View File

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

View File

@ -42,7 +42,7 @@
#include <functional>
#include <quazip/JlCompress.h>
#include <nonstd/optional>
#include <optional>
namespace MMCZip
{
@ -95,7 +95,7 @@ namespace MMCZip
/**
* Extract a subdirectory from an archive
*/
nonstd::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target);
std::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target);
bool extractRelFile(QuaZip *zip, const QString & file, const QString &target);
@ -106,7 +106,7 @@ namespace MMCZip
* \param dir The directory to extract to, the current directory if left empty.
* \return The list of the full paths of the files extracted, empty on failure.
*/
nonstd::optional<QStringList> extractDir(QString fileCompressed, QString dir);
std::optional<QStringList> extractDir(QString fileCompressed, QString dir);
/**
* Extract a subdirectory from an archive
@ -116,7 +116,7 @@ namespace MMCZip
* \param dir The directory to extract to, the current directory if left empty.
* \return The list of the full paths of the files extracted, empty on failure.
*/
nonstd::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir);
std::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir);
/**
* Extract a single file from an archive into a directory

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

View File

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

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;
}
#endif
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
// Strip IBus
// IBus is a Linux IME framework. For some reason, it breaks MC?
@ -173,11 +174,17 @@ JavaInstallPtr JavaUtils::GetDefaultJava()
QStringList addJavasFromEnv(QList<QString> javas)
{
QByteArray env = qgetenv("SNEEDMC_JAVA_PATHS");
QString env = qgetenv("SNEEDMC_JAVA_PATHS");
#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
QList<QString> javaPaths = QString::fromLocal8Bit(env).split(QLatin1String(":"));
QList<QString> javaPaths = env.split(':');
#endif
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 logLines(result.errorLog.split('\n'), MessageLevel::Error);
emit logLine(QString("\nCheck your SneedMC Java settings."), MessageLevel::Launcher);
printSystemInfo(false, false);
emitFailed(QString("Could not start java!"));
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 logLines(result.outLog.split('\n'), MessageLevel::Warning);
emit logLine("\nMinecraft might not start properly.", MessageLevel::Launcher);
printSystemInfo(false, false);
emitSucceeded();
return;
}
@ -138,7 +136,6 @@ void CheckJava::checkJavaFinished(JavaCheckResult result)
{
auto instance = m_parent->instance();
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("JavaArchitecture", result.mojangPlatform);
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")
.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:
void printJavaInfo(const QString & version, const QString & architecture, const QString & realArchitecture, const QString & vendor);
void printSystemInfo(bool javaIsKnown, bool javaIs64bit);
private:
QString m_javaPath;
qlonglong m_javaUnixTime;
JavaCheckerPtr m_JavaChecker;
};

View File

@ -140,6 +140,13 @@ VersionPtr VersionList::getVersion(const QString &version)
return out;
}
bool VersionList::hasVersion(QString version) const
{
auto ver = std::find_if(m_versions.constBegin(), m_versions.constEnd(),
[&](Meta::VersionPtr const& a){ return a->version() == version; });
return (ver != m_versions.constEnd());
}
void VersionList::setName(const QString &name)
{
m_name = name;

View File

@ -66,6 +66,7 @@ public:
QString humanReadable() const;
VersionPtr getVersion(const QString &version);
bool hasVersion(QString version) 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/Index.h>
#include "Component.h"
@ -60,7 +95,7 @@ void Component::applyTo(LaunchProfile* profile)
auto vfile = getVersionFile();
if(vfile)
{
vfile->applyTo(profile);
vfile->applyTo(profile, m_parent->runtimeContext());
}
else
{

View File

@ -173,9 +173,9 @@ void LaunchProfile::applyCompatibleJavaMajors(QList<int>& 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;
}
@ -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;
}
@ -221,10 +221,10 @@ void LaunchProfile::applyMavenFile(LibraryPtr mavenFile)
m_mavenFiles.append(Library::limitedCopy(mavenFile));
}
void LaunchProfile::applyAgent(AgentPtr agent)
void LaunchProfile::applyAgent(AgentPtr agent, const RuntimeContext & runtimeContext)
{
auto lib = agent->library();
if(!lib->isActive())
if(!lib->isActive(runtimeContext))
{
return;
}
@ -354,7 +354,7 @@ const QList<int> & LaunchProfile::getCompatibleJavaMajors() const
}
void LaunchProfile::getLibraryFiles(
const QString& architecture,
const RuntimeContext & runtimeContext,
QStringList& jars,
QStringList& nativeJars,
const QString& overridePath,
@ -366,7 +366,7 @@ void LaunchProfile::getLibraryFiles(
nativeJars.clear();
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
if(m_mainJar)
@ -379,18 +379,18 @@ void LaunchProfile::getLibraryFiles(
}
else
{
m_mainJar->getApplicableFiles(currentSystem, jars, nativeJars, native32, native64, overridePath);
m_mainJar->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath);
}
}
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);
}
else if(architecture == "64")
else if(runtimeContext.javaArchitecture == "64")
{
nativeJars.append(native64);
}

View File

@ -56,9 +56,9 @@ public: /* application of profile variables from patches */
void applyTweakers(const QStringList &tweakers);
void applyJarMods(const QList<LibraryPtr> &jarMods);
void applyMods(const QList<LibraryPtr> &jarMods);
void applyLibrary(LibraryPtr library);
void applyMavenFile(LibraryPtr library);
void applyAgent(AgentPtr agent);
void applyLibrary(LibraryPtr library, const RuntimeContext & runtimeContext);
void applyMavenFile(LibraryPtr library, const RuntimeContext & runtimeContext);
void applyAgent(AgentPtr agent, const RuntimeContext & runtimeContext);
void applyCompatibleJavaMajors(QList<int>& javaMajor);
void applyMainJar(LibraryPtr jar);
void applyProblemSeverity(ProblemSeverity severity);
@ -83,7 +83,7 @@ public: /* getters for profile variables */
const QList<int> & getCompatibleJavaMajors() const;
const LibraryPtr getMainJar() const;
void getLibraryFiles(
const QString & architecture,
const RuntimeContext & runtimeContext,
QStringList & jars,
QStringList & nativeJars,
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 "MinecraftInstance.h"
@ -7,7 +42,7 @@
#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
{
bool local = isLocal();
@ -21,7 +56,7 @@ void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& na
}
return out.absoluteFilePath();
};
QString raw_storage = storageSuffix(system);
QString raw_storage = storageSuffix(runtimeContext);
if(isNative())
{
if (raw_storage.contains("${arch}"))
@ -45,7 +80,7 @@ void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& na
}
QList<NetAction::Ptr> Library::getDownloads(
OpSys system,
const RuntimeContext & runtimeContext,
class HttpMetaCache* cache,
QStringList& failedLocalFiles,
const QString & overridePath
@ -88,6 +123,9 @@ QList<NetAction::Ptr> Library::getDownloads(
options |= Net::Download::Option::AcceptLocalFiles;
}
// Don't add a time limit for the libraries cache entry validity
options |= Net::Download::Option::MakeEternal;
if(sha1.size())
{
auto rawSha1 = QByteArray::fromHex(sha1.toLatin1());
@ -104,14 +142,14 @@ QList<NetAction::Ptr> Library::getDownloads(
return true;
};
QString raw_storage = storageSuffix(system);
QString raw_storage = storageSuffix(runtimeContext);
if(m_mojangDownloads)
{
if(isNative())
{
if(m_nativeClassifiers.contains(system))
auto nativeClassifier = getCompatibleNative(runtimeContext);
if(!nativeClassifier.isNull())
{
auto nativeClassifier = m_nativeClassifiers[system];
if(nativeClassifier.contains("${arch}"))
{
auto nat32Classifier = nativeClassifier;
@ -200,7 +238,7 @@ QList<NetAction::Ptr> Library::getDownloads(
return out;
}
bool Library::isActive() const
bool Library::isActive(const RuntimeContext & runtimeContext) const
{
bool result = true;
if (m_rules.empty())
@ -212,7 +250,7 @@ bool Library::isActive() const
RuleAction ruleResult = Disallow;
for (auto rule : m_rules)
{
RuleAction temp = rule->apply(this);
RuleAction temp = rule->apply(this, runtimeContext);
if (temp != Defer)
ruleResult = temp;
}
@ -220,7 +258,7 @@ bool Library::isActive() const
}
if (isNative())
{
result = result && m_nativeClassifiers.contains(currentSystem);
result = result && !getCompatibleNative(runtimeContext).isNull();
}
return result;
}
@ -235,6 +273,19 @@ bool Library::isAlwaysStale() const
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)
{
m_storagePrefix = prefix;
@ -254,7 +305,7 @@ QString Library::storagePrefix() const
return m_storagePrefix;
}
QString Library::filename(OpSys system) const
QString Library::filename(const RuntimeContext & runtimeContext) const
{
if(!m_filename.isEmpty())
{
@ -268,9 +319,10 @@ QString Library::filename(OpSys system) const
// otherwise native, override classifiers. Mojang HACK!
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
{
@ -279,14 +331,14 @@ QString Library::filename(OpSys system) const
return nativeSpec.getFileName();
}
QString Library::displayName(OpSys system) const
QString Library::displayName(const RuntimeContext & runtimeContext) const
{
if(!m_displayname.isEmpty())
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
if (!isNative())
@ -296,9 +348,10 @@ QString Library::storageSuffix(OpSys system) const
// otherwise native, override classifiers. Mojang HACK!
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
{

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
#include <QString>
#include <net/NetAction.h>
#include <QPair>
#include <QList>
#include <QString>
#include <QStringList>
#include <QMap>
#include <QDir>
@ -10,9 +46,9 @@
#include <memory>
#include "Rule.h"
#include "minecraft/OpSys.h"
#include "GradleSpecifier.h"
#include "MojangDownloadInfo.h"
#include "RuntimeContext.h"
class Library;
class MinecraftInstance;
@ -98,7 +134,7 @@ public: /* methods */
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;
void setAbsoluteUrl(const QString &absolute_url)
@ -112,7 +148,7 @@ public: /* methods */
}
/// 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
void setDisplayName(const QString & displayName)
@ -121,7 +157,7 @@ public: /* methods */
}
/// Get the file name of the library
QString displayName(OpSys system) const;
QString displayName(const RuntimeContext & runtimeContext) const;
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)
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
bool isLocal() const;
@ -152,9 +188,11 @@ public: /* methods */
bool isForge() const;
// 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;
QString getCompatibleNative(const RuntimeContext & runtimeContext) const;
private: /* methods */
/// the default storage prefix used by SneedMC
static QString defaultStoragePrefix();
@ -163,7 +201,7 @@ private: /* methods */
QString storagePrefix() const;
/// 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
{
@ -204,7 +242,7 @@ protected: /* data */
QStringList m_extractExcludes;
/// native suffixes per OS
QMap<OpSys, QString> m_nativeClassifiers;
QMap<QString, QString> m_nativeClassifiers;
/// true if the library had a rules section (even empty)
bool applyRules = false;

View File

@ -75,6 +75,7 @@
#include "mod/ModFolderModel.h"
#include "mod/ResourcePackFolderModel.h"
#include "mod/ShaderPackFolderModel.h"
#include "mod/TexturePackFolderModel.h"
#include "WorldList.h"
@ -115,6 +116,19 @@ private:
MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir)
: BaseInstance(globalSettings, settings, rootDir)
{
m_components.reset(new PackProfile(this));
}
void MinecraftInstance::saveNow()
{
m_components->saveNow();
}
void MinecraftInstance::loadSpecificSettings()
{
if (isSpecificSettingsLoaded())
return;
// Java Settings
auto javaOverride = m_settings->registerSetting("OverrideJava", false);
auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false);
@ -124,64 +138,66 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO
auto javaOrLocation = std::make_shared<OrSetting>("JavaOrLocationOverride", javaOverride, locationOverride);
auto javaOrArgs = std::make_shared<OrSetting>("JavaOrArgsOverride", javaOverride, argsOverride);
m_settings->registerOverride(globalSettings->getSetting("JavaPath"), javaOrLocation);
m_settings->registerOverride(globalSettings->getSetting("JvmArgs"), javaOrArgs);
m_settings->registerOverride(globalSettings->getSetting("IgnoreJavaCompatibility"), javaOrLocation);
if (auto global_settings = globalSettings()) {
m_settings->registerOverride(global_settings->getSetting("JavaPath"), javaOrLocation);
m_settings->registerOverride(global_settings->getSetting("JvmArgs"), javaOrArgs);
m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), javaOrLocation);
// special!
m_settings->registerPassthrough(globalSettings->getSetting("JavaTimestamp"), javaOrLocation);
m_settings->registerPassthrough(globalSettings->getSetting("JavaVersion"), javaOrLocation);
m_settings->registerPassthrough(globalSettings->getSetting("JavaArchitecture"), javaOrLocation);
// special!
m_settings->registerPassthrough(global_settings->getSetting("JavaTimestamp"), javaOrLocation);
m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation);
m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), javaOrLocation);
m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), javaOrLocation);
// Window Size
auto windowSetting = m_settings->registerSetting("OverrideWindow", false);
m_settings->registerOverride(globalSettings->getSetting("LaunchMaximized"), windowSetting);
m_settings->registerOverride(globalSettings->getSetting("MinecraftWinWidth"), windowSetting);
m_settings->registerOverride(globalSettings->getSetting("MinecraftWinHeight"), windowSetting);
// Window Size
auto windowSetting = m_settings->registerSetting("OverrideWindow", false);
m_settings->registerOverride(global_settings->getSetting("LaunchMaximized"), windowSetting);
m_settings->registerOverride(global_settings->getSetting("MinecraftWinWidth"), windowSetting);
m_settings->registerOverride(global_settings->getSetting("MinecraftWinHeight"), windowSetting);
// Memory
auto memorySetting = m_settings->registerSetting("OverrideMemory", false);
m_settings->registerOverride(globalSettings->getSetting("MinMemAlloc"), memorySetting);
m_settings->registerOverride(globalSettings->getSetting("MaxMemAlloc"), memorySetting);
m_settings->registerOverride(globalSettings->getSetting("PermGen"), memorySetting);
// Memory
auto memorySetting = m_settings->registerSetting("OverrideMemory", false);
m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting);
m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting);
m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting);
// Minecraft launch method
auto launchMethodOverride = m_settings->registerSetting("OverrideMCLaunchMethod", false);
m_settings->registerOverride(globalSettings->getSetting("MCLaunchMethod"), launchMethodOverride);
// Minecraft launch method
auto launchMethodOverride = m_settings->registerSetting("OverrideMCLaunchMethod", false);
m_settings->registerOverride(global_settings->getSetting("MCLaunchMethod"), launchMethodOverride);
// Native library workarounds
auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false);
m_settings->registerOverride(globalSettings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride);
m_settings->registerOverride(globalSettings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride);
// Native library workarounds
auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false);
m_settings->registerOverride(global_settings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride);
m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride);
// Peformance related options
auto performanceOverride = m_settings->registerSetting("OverridePerformance", false);
m_settings->registerOverride(globalSettings->getSetting("EnableFeralGamemode"), performanceOverride);
m_settings->registerOverride(globalSettings->getSetting("EnableMangoHud"), performanceOverride);
m_settings->registerOverride(globalSettings->getSetting("UseDiscreteGpu"), performanceOverride);
// Peformance related options
auto performanceOverride = m_settings->registerSetting("OverridePerformance", false);
m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride);
m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride);
m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride);
// Game time
auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false);
m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride);
m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"), gameTimeOverride);
// Miscellaneous
auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false);
m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride);
m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride);
m_settings->set("InstanceType", "OneSix");
}
// Join server on launch, this does not have a global override
m_settings->registerSetting("JoinServerOnLaunch", false);
m_settings->registerSetting("JoinServerOnLaunchAddress", "");
// Miscellaneous
auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false);
m_settings->registerOverride(globalSettings->getSetting("CloseAfterLaunch"), miscellaneousOverride);
m_settings->registerOverride(globalSettings->getSetting("QuitAfterGameStop"), miscellaneousOverride);
qDebug() << "Instance-type specific settings were loaded!";
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
@ -237,6 +253,14 @@ QString MinecraftInstance::getLocalLibraryPath() const
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
{
QDir jarmods_dir(FS::PathCombine(instanceRoot(), "jarmods/"));
@ -308,12 +332,11 @@ QDir MinecraftInstance::versionsPath() const
return QDir::current().absoluteFilePath("versions");
}
QStringList MinecraftInstance::getClassPath() const
QStringList MinecraftInstance::getClassPath()
{
QStringList jars, nativeJars;
auto javaArchitecture = settings()->get("JavaArchitecture").toString();
auto profile = m_components->getProfile();
profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot());
profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot());
return jars;
}
@ -323,16 +346,15 @@ QString MinecraftInstance::getMainClass() const
return profile->getMainClass();
}
QStringList MinecraftInstance::getNativeJars() const
QStringList MinecraftInstance::getNativeJars()
{
QStringList jars, nativeJars;
auto javaArchitecture = settings()->get("JavaArchitecture").toString();
auto profile = m_components->getProfile();
profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot());
profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot());
return nativeJars;
}
QStringList MinecraftInstance::extraArguments() const
QStringList MinecraftInstance::extraArguments()
{
auto list = BaseInstance::extraArguments();
auto version = getPackProfile();
@ -352,13 +374,13 @@ QStringList MinecraftInstance::extraArguments() const
for (auto agent : agents)
{
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()));
}
return list;
}
QStringList MinecraftInstance::javaArguments() const
QStringList MinecraftInstance::javaArguments()
{
QStringList args;
@ -415,7 +437,7 @@ QStringList MinecraftInstance::javaArguments() const
return args;
}
QMap<QString, QString> MinecraftInstance::getVariables() const
QMap<QString, QString> MinecraftInstance::getVariables()
{
QMap<QString, QString> out;
out.insert("INST_NAME", name());
@ -447,13 +469,11 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
QProcessEnvironment env = createEnvironment();
#ifdef Q_OS_LINUX
if (settings()->get("EnableMangoHud").toBool())
if (settings()->get("EnableMangoHud").toBool() && APPLICATION->capabilities() & Application::SupportsMangoHud)
{
auto preload = env.value("LD_PRELOAD", "") + ":libMangoHud_dlsym.so:libMangoHud.so";
auto lib_path = env.value("LD_LIBRARY_PATH", "") + ":/usr/local/$LIB/mangohud/:/usr/$LIB/mangohud/";
env.insert("LD_PRELOAD", preload);
env.insert("LD_LIBRARY_PATH", lib_path);
env.insert("MANGOHUD", "1");
}
@ -612,8 +632,7 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS
// libraries and class path.
{
QStringList jars, nativeJars;
auto javaArchitecture = settings()->get("JavaArchitecture").toString();
profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot());
profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot());
for(auto file: jars)
{
launchScript += "cp " + file + "\n";
@ -669,8 +688,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
{
out << "Libraries:";
QStringList jars, nativeJars;
auto javaArchitecture = settings->get("JavaArchitecture").toString();
profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot());
profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot());
auto printLibFile = [&](const QString & path)
{
QFileInfo info(path);
@ -701,14 +719,14 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
{
out << QString("%1:").arg(label);
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 bName = b->fileinfo().completeBaseName();
return aName.localeAwareCompare(bName) < 0;
});
for(auto mod: modList)
{
if(mod->type() == Mod::MOD_FOLDER)
if(mod->type() == ResourceType::FOLDER)
{
out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)";
continue;
@ -735,8 +753,8 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
out << "Jar Mods:";
for(auto & jarmod: jarMods)
{
auto displayname = jarmod->displayName(currentSystem);
auto realname = jarmod->filename(currentSystem);
auto displayname = jarmod->displayName(runtimeContext());
auto realname = jarmod->filename(runtimeContext());
if(displayname != realname)
{
out << " " + displayname + " (" + realname + ")";
@ -898,6 +916,7 @@ QString MinecraftInstance::getStatusbarDescription()
Task::Ptr MinecraftInstance::createUpdateTask(Net::Mode mode)
{
updateRuntimeContext();
switch (mode)
{
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)
{
updateRuntimeContext();
// FIXME: get rid of shared_from_this ...
auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(shared_from_this()));
auto pptr = process.get();
@ -944,9 +964,9 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(new CreateGameFolders(pptr));
}
if (!serverToJoin && m_settings->get("JoinServerOnLaunch").toBool())
if (!serverToJoin && settings()->get("JoinServerOnLaunch").toBool())
{
QString fullAddress = m_settings->get("JoinServerOnLaunchAddress").toString();
QString fullAddress = settings()->get("JoinServerOnLaunchAddress").toString();
serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(fullAddress)));
}
@ -1054,10 +1074,10 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
QString MinecraftInstance::launchMethod()
{
return m_settings->get("MCLaunchMethod").toString();
return settings()->get("MCLaunchMethod").toString();
}
JavaVersion MinecraftInstance::getJavaVersion() const
JavaVersion MinecraftInstance::getJavaVersion()
{
return JavaVersion(settings()->get("JavaVersion").toString());
}
@ -1086,18 +1106,18 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const
return m_core_mod_list;
}
std::shared_ptr<ModFolderModel> MinecraftInstance::resourcePackList() const
std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList() const
{
if (!m_resource_pack_list)
{
m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir()));
m_resource_pack_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ModFolderModel::disableInteraction);
m_resource_pack_list->enableInteraction(!isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ResourcePackFolderModel::disableInteraction);
}
return m_resource_pack_list;
}
std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList() const
{
if (!m_texture_pack_list)
{
@ -1108,11 +1128,11 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
return m_texture_pack_list;
}
std::shared_ptr<ModFolderModel> MinecraftInstance::shaderPackList() const
std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList() const
{
if (!m_shader_pack_list)
{
m_shader_pack_list.reset(new ResourcePackFolderModel(shaderPacksDir()));
m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir()));
m_shader_pack_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_shader_pack_list.get(), &ModFolderModel::disableInteraction);
}
@ -1144,7 +1164,7 @@ QList<Mod*> MinecraftInstance::getJarMods() const
for (auto jarmod : profile->getJarMods())
{
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));
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
#include "BaseInstance.h"
#include <java/JavaVersion.h>
@ -7,6 +42,10 @@
#include "minecraft/launch/MinecraftServerTarget.h"
class ModFolderModel;
class ResourceFolderModel;
class ResourcePackFolderModel;
class ShaderPackFolderModel;
class TexturePackFolderModel;
class WorldList;
class GameOptions;
class LaunchStep;
@ -20,6 +59,8 @@ public:
virtual ~MinecraftInstance() {};
virtual void saveNow() override;
void loadSpecificSettings() override;
// FIXME: remove
QString typeName() const override;
// FIXME: remove
@ -63,6 +104,10 @@ public:
// where the instance-local libraries should be
QString getLocalLibraryPath() const;
/** Returns whether the instance, with its version, has support for demo mode. */
[[nodiscard]] bool supportsDemo() const;
void updateRuntimeContext();
////// Profile management //////
std::shared_ptr<PackProfile> getPackProfile() const;
@ -70,24 +115,24 @@ public:
////// Mod Lists //////
std::shared_ptr<ModFolderModel> loaderModList() const;
std::shared_ptr<ModFolderModel> coreModList() const;
std::shared_ptr<ModFolderModel> resourcePackList() const;
std::shared_ptr<ModFolderModel> texturePackList() const;
std::shared_ptr<ModFolderModel> shaderPackList() const;
std::shared_ptr<ResourcePackFolderModel> resourcePackList() const;
std::shared_ptr<TexturePackFolderModel> texturePackList() const;
std::shared_ptr<ShaderPackFolderModel> shaderPackList() const;
std::shared_ptr<WorldList> worldList() const;
std::shared_ptr<GameOptions> gameOptionsModel() const;
////// Launch stuff //////
Task::Ptr createUpdateTask(Net::Mode mode) override;
shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override;
QStringList extraArguments() const override;
QStringList extraArguments() override;
QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override;
QList<Mod*> getJarMods() const;
QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin);
/// get arguments passed to java
QStringList javaArguments() const;
QStringList javaArguments();
/// get variables for launch command variable substitution/environment
QMap<QString, QString> getVariables() const override;
QMap<QString, QString> getVariables() override;
/// create an environment for launching processes
QProcessEnvironment createEnvironment() override;
@ -103,16 +148,16 @@ public:
QString getStatusbarDescription() override;
// FIXME: remove
virtual QStringList getClassPath() const;
virtual QStringList getClassPath();
// FIXME: remove
virtual QStringList getNativeJars() const;
virtual QStringList getNativeJars();
// FIXME: remove
virtual QString getMainClass() const;
// FIXME: remove
virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) const;
virtual JavaVersion getJavaVersion() const;
virtual JavaVersion getJavaVersion();
protected:
QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session);
@ -123,9 +168,9 @@ protected: // data
std::shared_ptr<PackProfile> m_components;
mutable std::shared_ptr<ModFolderModel> m_loader_mod_list;
mutable std::shared_ptr<ModFolderModel> m_core_mod_list;
mutable std::shared_ptr<ModFolderModel> m_resource_pack_list;
mutable std::shared_ptr<ModFolderModel> m_shader_pack_list;
mutable std::shared_ptr<ModFolderModel> m_texture_pack_list;
mutable std::shared_ptr<ResourcePackFolderModel> m_resource_pack_list;
mutable std::shared_ptr<ShaderPackFolderModel> m_shader_pack_list;
mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list;
mutable std::shared_ptr<WorldList> m_world_list;
mutable std::shared_ptr<GameOptions> m_game_options;
};

View File

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

View File

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

View File

@ -362,11 +362,8 @@ LibraryPtr MojangVersionFormat::libraryFromJson(ProblemContainer & problems, con
{
qWarning() << filename << "contains an invalid native (skipping)";
}
OpSys opSys = OpSys_fromString(it.key());
if (opSys != Os_Other)
{
out->m_nativeClassifiers[opSys] = it.value().toString();
}
// FIXME: Skip unknown platforms
out->m_nativeClassifiers[it.key()] = it.value().toString();
}
}
if (libObj.contains("rules"))
@ -395,7 +392,7 @@ QJsonObject MojangVersionFormat::libraryToJson(Library *library)
auto iter = library->m_nativeClassifiers.begin();
while (iter != library->m_nativeClassifiers.end())
{
nativeList.insert(OpSys_toString(iter.key()), iter.value());
nativeList.insert(iter.key(), iter.value());
iter++;
}
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();
}
RuntimeContext PackProfile::runtimeContext()
{
return d->m_instance->runtimeContext();
}
QString PackProfile::componentsFilePath() const
{
return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json");
@ -784,7 +789,7 @@ bool PackProfile::removeComponent_internal(ComponentPtr patch)
return true;
}
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]);
if(finfo.exists())
{

View File

@ -1,16 +1,36 @@
/* Copyright 2013-2021 MultiMC Contributors
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* http://www.apache.org/licenses/LICENSE-2.0
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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
@ -104,6 +124,9 @@ public:
/// if there is a save scheduled, do it now.
void saveNow();
/// helper method, returns RuntimeContext of instance
RuntimeContext runtimeContext();
signals:
void minecraftChanged();

View File

@ -1,16 +1,36 @@
/* Copyright 2013-2021 MultiMC Contributors
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* http://www.apache.org/licenses/LICENSE-2.0
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 <QJsonObject>
@ -60,10 +80,10 @@ QList<std::shared_ptr<Rule>> rulesFromJsonV4(const QJsonObject &objectWithRules)
auto osNameVal = osObj.value("name");
if (!osNameVal.isString())
continue;
OpSys requiredOs = OpSys_fromString(osNameVal.toString());
QString osName = osNameVal.toString();
QString versionRegex = osObj.value("version").toString();
// add a new OS rule
rules.append(OsRule::create(action, requiredOs, versionRegex));
rules.append(OsRule::create(action, osName, versionRegex));
}
return rules;
}
@ -81,7 +101,7 @@ QJsonObject OsRule::toJson()
ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow"));
QJsonObject osObj;
{
osObj.insert("name", OpSys_toString(m_system));
osObj.insert("name", m_system);
if(!m_version_regexp.isEmpty())
{
osObj.insert("version", m_version_regexp);

View File

@ -1,16 +1,36 @@
/* Copyright 2013-2021 MultiMC Contributors
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* http://www.apache.org/licenses/LICENSE-2.0
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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
@ -19,7 +39,7 @@
#include <QList>
#include <QJsonObject>
#include <memory>
#include "OpSys.h"
#include "RuntimeContext.h"
class Library;
class Rule;
@ -37,7 +57,7 @@ class Rule
{
protected:
RuleAction m_result;
virtual bool applies(const Library *parent) = 0;
virtual bool applies(const Library *parent, const RuntimeContext & runtimeContext) = 0;
public:
Rule(RuleAction result) : m_result(result)
@ -45,9 +65,9 @@ public:
}
virtual ~Rule() {};
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;
else
return Defer;
@ -58,23 +78,23 @@ class OsRule : public Rule
{
private:
// the OS
OpSys m_system;
QString m_system;
// the OS version regexp
QString m_version_regexp;
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)
{
}
public:
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)
{
return std::shared_ptr<OsRule>(new OsRule(result, system, version_regexp));
@ -84,7 +104,7 @@ public:
class ImplicitRule : public Rule
{
protected:
virtual bool applies(const Library *)
virtual bool applies(const Library *, const RuntimeContext & runtimeContext)
{
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";
}
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.
if (isMinecraftVersion(uid))
@ -77,15 +77,15 @@ void VersionFile::applyTo(LaunchProfile *profile)
for (auto library : libraries)
{
profile->applyLibrary(library);
profile->applyLibrary(library, runtimeContext);
}
for (auto mavenFile : mavenFiles)
{
profile->applyMavenFile(mavenFile);
profile->applyMavenFile(mavenFile, runtimeContext);
}
for (auto agent : agents)
{
profile->applyAgent(agent);
profile->applyAgent(agent, runtimeContext);
}
profile->applyProblemSeverity(getProblemSeverity());
}

View File

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

View File

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

View File

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

View File

@ -109,8 +109,10 @@ QStringList AccountList::profileNames() const {
void AccountList::addAccount(const MinecraftAccountPtr account)
{
// NOTE: Do not allow adding something that's already there
if(m_accounts.contains(account)) {
// 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)) {
qDebug() << "Tried to add account that's already on the accounts list!";
return;
}
@ -123,6 +125,8 @@ void AccountList::addAccount(const MinecraftAccountPtr account)
if(profileId.size()) {
auto existingAccount = findAccountByProfileId(profileId);
if(existingAccount != -1) {
qDebug() << "Replacing old account with a new one with the same profile ID!";
MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount];
m_accounts[existingAccount] = account;
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
int row = m_accounts.count();
qDebug() << "Inserting account at index" << row;
beginInsertRows(QModelIndex(), row, row);
m_accounts.append(account);
endInsertRows();
onListChanged();
}

View File

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

View File

@ -21,6 +21,8 @@
#include <FileSystem.h>
#include <Commandline.h>
#include "Application.h"
#ifdef Q_OS_LINUX
#include "gamemode_client.h"
#endif
@ -86,7 +88,7 @@ void DirectJavaLaunch::executeTask()
}
#ifdef Q_OS_LINUX
if (instance->settings()->get("EnableFeralGamemode").toBool())
if (instance->settings()->get("EnableFeralGamemode").toBool() && APPLICATION->capabilities() & Application::SupportsGameMode)
{
auto pid = m_process.processId();
if (pid)

View File

@ -181,7 +181,7 @@ void LauncherPartLaunch::executeTask()
}
#ifdef Q_OS_LINUX
if (instance->settings()->get("EnableFeralGamemode").toBool())
if (instance->settings()->get("EnableFeralGamemode").toBool() && APPLICATION->capabilities() & Application::SupportsGameMode)
{
auto pid = m_process.processId();
if (pid)

View File

@ -1,22 +1,41 @@
/* Copyright 2013-2021 MultiMC Contributors
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* http://www.apache.org/licenses/LICENSE-2.0
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ModMinecraftJar.h"
#include "launch/LaunchTask.h"
#include "MMCZip.h"
#include "minecraft/OpSys.h"
#include "FileSystem.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
@ -50,7 +69,7 @@ void ModMinecraftJar::executeTask()
{
auto mainJar = profile->getMainJar();
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];
if(!MMCZip::createModdedJar(sourceJarPath, finalJarPath, jarMods))
{

View File

@ -1,22 +1,41 @@
/* Copyright 2013-2021 MultiMC Contributors
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* http://www.apache.org/licenses/LICENSE-2.0
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 "ScanModFolders.h"
#include "launch/LaunchTask.h"
#include "MMCZip.h"
#include "minecraft/OpSys.h"
#include "FileSystem.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/mod/ModFolderModel.h"

View File

@ -36,130 +36,77 @@
#include "Mod.h"
#include <QDebug>
#include <QDir>
#include <QString>
#include <QRegularExpression>
#include <FileSystem.h>
#include <QDebug>
#include "Application.h"
#include "MetadataHandler.h"
#include "Version.h"
namespace {
ModDetails invalidDetails;
}
Mod::Mod(const QFileInfo& file)
Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
{
repath(file);
m_changedDateTime = file.lastModified();
m_enabled = (file.suffix() != "disabled");
}
Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata)
: m_file(mods_dir.absoluteFilePath(metadata.filename))
, m_internal_id(metadata.filename)
, m_name(metadata.name)
: Mod(mods_dir.absoluteFilePath(metadata.filename))
{
if (m_file.isDir()) {
m_type = MOD_FOLDER;
} else {
if (metadata.filename.endsWith(".zip") || metadata.filename.endsWith(".jar"))
m_type = MOD_ZIPFILE;
else if (metadata.filename.endsWith(".litemod"))
m_type = MOD_LITEMOD;
else
m_type = MOD_SINGLEFILE;
}
m_enabled = true;
m_changedDateTime = m_file.lastModified();
m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
}
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;
m_name = metadata.name;
m_local_details.metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
}
void Mod::setStatus(ModStatus status)
{
if (m_localDetails) {
m_localDetails->status = status;
} else {
m_temp_status = status;
}
m_local_details.status = status;
}
void Mod::setMetadata(const Metadata::ModStruct& metadata)
void Mod::setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata)
{
if (status() == ModStatus::NoMetadata)
setStatus(ModStatus::Installed);
if (m_localDetails) {
m_localDetails->metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
} else {
m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
m_local_details.metadata = 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
@ -175,13 +122,12 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
}
}
m_type = MOD_UNKNOWN;
return FS::deletePath(m_file.filePath());
return Resource::destroy();
}
auto Mod::details() const -> const ModDetails&
{
return m_localDetails ? *m_localDetails : invalidDetails;
return m_local_details;
}
auto Mod::name() const -> QString
@ -218,35 +164,29 @@ auto Mod::authors() const -> QStringList
auto Mod::status() const -> ModStatus
{
if (!m_localDetails)
return m_temp_status;
return details().status;
}
auto Mod::metadata() -> std::shared_ptr<Metadata::ModStruct>
{
if (m_localDetails)
return m_localDetails->metadata;
return m_temp_metadata;
return m_local_details.metadata;
}
auto Mod::metadata() const -> const std::shared_ptr<Metadata::ModStruct>
{
if (m_localDetails)
return m_localDetails->metadata;
return m_temp_metadata;
return m_local_details.metadata;
}
void Mod::finishResolvingWithDetails(std::shared_ptr<ModDetails> details)
void Mod::finishResolvingWithDetails(ModDetails&& details)
{
m_resolving = false;
m_resolved = true;
m_localDetails = details;
m_is_resolving = false;
m_is_resolved = true;
setStatus(m_temp_status);
std::shared_ptr<Metadata::ModStruct> metadata = details.metadata;
if (details.status == ModStatus::Unknown)
details.status = m_local_details.status;
if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) {
setMetadata(*m_temp_metadata);
m_temp_metadata.reset();
}
m_local_details = std::move(details);
if (metadata)
setMetadata(std::move(metadata));
}

View File

@ -39,38 +39,23 @@
#include <QFileInfo>
#include <QList>
#include "QObjectPtr.h"
#include "Resource.h"
#include "ModDetails.h"
class Mod : public QObject
class Mod : public Resource
{
Q_OBJECT
public:
enum ModType
{
MOD_UNKNOWN, //!< Indicates an unspecified mod type.
MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files.
MOD_SINGLEFILE, //!< The mod is a single file (not a zip file).
MOD_FOLDER, //!< The mod is in a folder on the filesystem.
MOD_LITEMOD, //!< The mod is a litemod
};
using Ptr = shared_qobject_ptr<Mod>;
using WeakPtr = QPointer<Mod>;
Mod() = default;
Mod(const QFileInfo &file);
explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
auto fileinfo() const -> QFileInfo { return m_file; }
auto dateTimeChanged() const -> QDateTime { return m_changedDateTime; }
auto internal_id() const -> QString { return m_internal_id; }
auto type() const -> ModType { return m_type; }
auto enabled() const -> bool { return m_enabled; }
auto valid() const -> bool { return m_type != MOD_UNKNOWN; }
Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
Mod(QString file_path) : Mod(QFileInfo(file_path)) {}
auto details() const -> const ModDetails&;
auto name() const -> QString;
auto name() const -> QString override;
auto version() const -> QString;
auto homeurl() const -> QString;
auto description() const -> QString;
@ -81,46 +66,17 @@ public:
auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>;
void setStatus(ModStatus status);
void setMetadata(const Metadata::ModStruct& metadata);
void setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata);
void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared<Metadata::ModStruct>(metadata)); }
auto enable(bool value) -> bool;
[[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
// delete all the files of this mod
// Delete all the files of this mod
auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool;
// change the mod's filesystem path (used by mod lists for *MAGIC* purposes)
void repath(const QFileInfo &file);
auto shouldResolve() const -> bool { return !m_resolving && !m_resolved; }
auto isResolving() const -> bool { return m_resolving; }
auto resolutionTicket() const -> int { return m_resolutionTicket; }
void setResolving(bool resolving, int resolutionTicket) {
m_resolving = resolving;
m_resolutionTicket = resolutionTicket;
}
void finishResolvingWithDetails(std::shared_ptr<ModDetails> details);
void finishResolvingWithDetails(ModDetails&& details);
protected:
QFileInfo m_file;
QDateTime m_changedDateTime;
QString m_internal_id;
/* Name as reported via the file name */
QString m_name;
ModType m_type = MOD_UNKNOWN;
/* If the mod has metadata, this will be filled in the constructor, and passed to
* the ModDetails when calling finishResolvingWithDetails */
std::shared_ptr<Metadata::ModStruct> m_temp_metadata;
/* Set the mod status while it doesn't have local details just yet */
ModStatus m_temp_status = ModStatus::NoMetadata;
std::shared_ptr<ModDetails> m_localDetails;
bool m_enabled = true;
bool m_resolving = false;
bool m_resolved = false;
int m_resolutionTicket = 0;
ModDetails m_local_details;
};

View File

@ -46,34 +46,77 @@ enum class ModStatus {
Installed, // Both JAR and Metadata are present
NotInstalled, // Only the Metadata is present
NoMetadata, // Only the JAR is present
Unknown, // Default status
};
struct ModDetails
{
/* Mod ID as defined in the ModLoader-specific metadata */
QString mod_id;
QString mod_id = {};
/* Human-readable name */
QString name;
QString name = {};
/* Human-readable mod version */
QString version;
QString version = {};
/* Human-readable minecraft version */
QString mcversion;
QString mcversion = {};
/* URL for mod's home page */
QString homeurl;
QString homeurl = {};
/* Human-readable description */
QString description;
QString description = {};
/* List of the author's names */
QStringList authors;
QStringList authors = {};
/* Installation status of the mod */
ModStatus status;
ModStatus status = ModStatus::Unknown;
/* Metadata information, if any */
std::shared_ptr<Metadata::ModStruct> metadata;
std::shared_ptr<Metadata::ModStruct> metadata = nullptr;
ModDetails() = default;
/** Metadata should be handled manually to properly set the mod status. */
ModDetails(ModDetails& other)
: mod_id(other.mod_id)
, name(other.name)
, version(other.version)
, mcversion(other.mcversion)
, homeurl(other.homeurl)
, description(other.description)
, authors(other.authors)
, status(other.status)
{}
ModDetails& operator=(ModDetails& other)
{
this->mod_id = other.mod_id;
this->name = other.name;
this->version = other.version;
this->mcversion = other.mcversion;
this->homeurl = other.homeurl;
this->description = other.description;
this->authors = other.authors;
this->status = other.status;
return *this;
}
ModDetails& operator=(ModDetails&& other)
{
this->mod_id = other.mod_id;
this->name = other.name;
this->version = other.version;
this->mcversion = other.mcversion;
this->homeurl = other.homeurl;
this->description = other.description;
this->authors = other.authors;
this->status = other.status;
return *this;
}
};

View File

@ -49,428 +49,52 @@
#include "minecraft/mod/tasks/LocalModParseTask.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : QAbstractListModel(), m_dir(dir), m_is_indexed(is_indexed)
ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(QDir(dir)), m_is_indexed(is_indexed)
{
FS::ensureFolderPathExists(m_dir.absolutePath());
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
m_watcher = new QFileSystemWatcher(this);
connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString)));
}
void ModFolderModel::startWatching()
{
if(is_watching)
return;
update();
// Watch the mods folder
is_watching = m_watcher->addPath(m_dir.absolutePath());
if (is_watching) {
qDebug() << "Started watching " << m_dir.absolutePath();
} else {
qDebug() << "Failed to start watching " << m_dir.absolutePath();
}
// Watch the mods index folder
is_watching = m_watcher->addPath(indexDir().absolutePath());
if (is_watching) {
qDebug() << "Started watching " << indexDir().absolutePath();
} else {
qDebug() << "Failed to start watching " << indexDir().absolutePath();
}
}
void ModFolderModel::stopWatching()
{
if(!is_watching)
return;
is_watching = !m_watcher->removePath(m_dir.absolutePath());
if (!is_watching) {
qDebug() << "Stopped watching " << m_dir.absolutePath();
} else {
qDebug() << "Failed to stop watching " << m_dir.absolutePath();
}
is_watching = !m_watcher->removePath(indexDir().absolutePath());
if (!is_watching) {
qDebug() << "Stopped watching " << indexDir().absolutePath();
} else {
qDebug() << "Failed to stop watching " << indexDir().absolutePath();
}
}
bool ModFolderModel::update()
{
if (!isValid()) {
return false;
}
if(m_update) {
scheduled_update = true;
return true;
}
auto index_dir = indexDir();
auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed);
m_update = task->result();
QThreadPool *threadPool = QThreadPool::globalInstance();
connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate);
threadPool->start(task);
return true;
}
void ModFolderModel::finishUpdate()
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
auto currentList = modsIndex.keys();
QSet<QString> currentSet(currentList.begin(), currentList.end());
auto & newMods = m_update->mods;
auto newList = newMods.keys();
QSet<QString> newSet(newList.begin(), newList.end());
#else
QSet<QString> currentSet = modsIndex.keys().toSet();
auto& newMods = m_update->mods;
QSet<QString> newSet = newMods.keys().toSet();
#endif
// see if the kept mods changed in some way
{
QSet<QString> kept = currentSet;
kept.intersect(newSet);
for(auto& keptMod : kept) {
auto newMod = newMods[keptMod];
auto row = modsIndex[keptMod];
auto currentMod = mods[row];
if(newMod->dateTimeChanged() == currentMod->dateTimeChanged()) {
// no significant change, ignore...
continue;
}
auto oldMod = mods[row];
if(oldMod->isResolving()) {
activeTickets.remove(oldMod->resolutionTicket());
}
mods[row] = newMod;
resolveMod(mods[row]);
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
}
}
// remove mods no longer present
{
QSet<QString> removed = currentSet;
QList<int> removedRows;
removed.subtract(newSet);
for(auto & removedMod: removed) {
removedRows.append(modsIndex[removedMod]);
}
std::sort(removedRows.begin(), removedRows.end(), std::greater<int>());
for(auto iter = removedRows.begin(); iter != removedRows.end(); iter++) {
int removedIndex = *iter;
beginRemoveRows(QModelIndex(), removedIndex, removedIndex);
auto removedIter = mods.begin() + removedIndex;
if((*removedIter)->isResolving()) {
activeTickets.remove((*removedIter)->resolutionTicket());
}
mods.erase(removedIter);
endRemoveRows();
}
}
// add new mods to the end
{
QSet<QString> added = newSet;
added.subtract(currentSet);
// When you have a Qt build with assertions turned on, proceeding here will abort the application
if (added.size() > 0) {
beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1);
for (auto& addedMod : added) {
mods.append(newMods[addedMod]);
resolveMod(mods.last());
}
endInsertRows();
}
}
// update index
{
modsIndex.clear();
int idx = 0;
for(auto mod: mods) {
modsIndex[mod->internal_id()] = idx;
idx++;
}
}
m_update.reset();
emit updateFinished();
if(scheduled_update) {
scheduled_update = false;
update();
}
}
void ModFolderModel::resolveMod(Mod::Ptr m)
{
if(!m->shouldResolve()) {
return;
}
auto task = new LocalModParseTask(nextResolutionTicket, m->type(), m->fileinfo());
auto result = task->result();
result->id = m->internal_id();
activeTickets.insert(nextResolutionTicket, result);
m->setResolving(true, nextResolutionTicket);
nextResolutionTicket++;
QThreadPool *threadPool = QThreadPool::globalInstance();
connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse);
threadPool->start(task);
}
void ModFolderModel::finishModParse(int token)
{
auto iter = activeTickets.find(token);
if(iter == activeTickets.end()) {
return;
}
auto result = *iter;
activeTickets.remove(token);
int row = modsIndex[result->id];
auto mod = mods[row];
mod->finishResolvingWithDetails(result->details);
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
}
void ModFolderModel::disableInteraction(bool disabled)
{
if (interaction_disabled == disabled) {
return;
}
interaction_disabled = disabled;
if(size()) {
emit dataChanged(index(0), index(size() - 1));
}
}
void ModFolderModel::directoryChanged(QString path)
{
update();
}
bool ModFolderModel::isValid()
{
return m_dir.exists() && m_dir.isReadable();
}
auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>
{
QList<Mod::Ptr> selected_mods;
for (auto i : indexes) {
if(i.column() != 0)
continue;
selected_mods.push_back(mods[i.row()]);
}
return selected_mods;
}
// FIXME: this does not take disabled mod (with extra .disable extension) into account...
bool ModFolderModel::installMod(const QString &filename)
{
if(interaction_disabled) {
return false;
}
// NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
auto originalPath = FS::NormalizePath(filename);
QFileInfo fileinfo(originalPath);
if (!fileinfo.exists() || !fileinfo.isReadable())
{
qWarning() << "Caught attempt to install non-existing file or file-like object:" << originalPath;
return false;
}
qDebug() << "installing: " << fileinfo.absoluteFilePath();
Mod installedMod(fileinfo);
if (!installedMod.valid())
{
qDebug() << originalPath << "is not a valid mod. Ignoring it.";
return false;
}
auto type = installedMod.type();
if (type == Mod::MOD_UNKNOWN)
{
qDebug() << "Cannot recognize mod type of" << originalPath << ", ignoring it.";
return false;
}
auto newpath = FS::NormalizePath(FS::PathCombine(m_dir.path(), fileinfo.fileName()));
if(originalPath == newpath)
{
qDebug() << "Overwriting the mod (" << originalPath << ") with itself makes no sense...";
return false;
}
if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD)
{
if(QFile::exists(newpath) || QFile::exists(newpath + QString(".disabled")))
{
if(!QFile::remove(newpath))
{
// FIXME: report error in a user-visible way
qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
return false;
}
qDebug() << newpath << "has been deleted.";
}
if (!QFile::copy(fileinfo.filePath(), newpath))
{
qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
// FIXME: report error in a user-visible way
return false;
}
FS::updateTimestamp(newpath);
QFileInfo newpathInfo(newpath);
installedMod.repath(newpathInfo);
update();
return true;
}
else if (type == Mod::MOD_FOLDER)
{
QString from = fileinfo.filePath();
if(QFile::exists(newpath))
{
qDebug() << "Ignoring folder " << from << ", it would merge with " << newpath;
return false;
}
if (!FS::copy(from, newpath)())
{
qWarning() << "Copy of folder from" << originalPath << "to" << newpath << "has (potentially partially) failed.";
return false;
}
QFileInfo newpathInfo(newpath);
installedMod.repath(newpathInfo);
update();
return true;
}
return false;
}
bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
{
for(auto mod : allMods()){
if(mod->fileinfo().fileName() == filename){
auto index_dir = indexDir();
mod->destroy(index_dir, preserve_metadata);
return true;
}
}
return false;
}
bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable)
{
if(interaction_disabled) {
return false;
}
if(indexes.isEmpty())
return true;
for (auto index: indexes)
{
if(index.column() != 0) {
continue;
}
setModStatus(index.row(), enable);
}
return true;
}
bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
{
if(interaction_disabled) {
return false;
}
if(indexes.isEmpty())
return true;
for (auto i: indexes)
{
if(i.column() != 0) {
continue;
}
auto m = mods[i.row()];
auto index_dir = indexDir();
m->destroy(index_dir);
}
return true;
}
int ModFolderModel::columnCount(const QModelIndex &parent) const
{
return NUM_COLUMNS;
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE };
}
QVariant ModFolderModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (!validateIndex(index))
return {};
int row = index.row();
int column = index.column();
if (row < 0 || row >= mods.size())
return QVariant();
switch (role)
{
case Qt::DisplayRole:
switch (column)
{
case NameColumn:
return mods[row]->name();
return m_resources[row]->name();
case VersionColumn: {
switch(mods[row]->type()) {
case Mod::MOD_FOLDER:
switch(m_resources[row]->type()) {
case ResourceType::FOLDER:
return tr("Folder");
case Mod::MOD_SINGLEFILE:
case ResourceType::SINGLEFILE:
return tr("File");
default:
break;
}
return mods[row]->version();
return at(row)->version();
}
case DateColumn:
return mods[row]->dateTimeChanged();
return m_resources[row]->dateTimeChanged();
default:
return QVariant();
}
case Qt::ToolTipRole:
return mods[row]->internal_id();
return m_resources[row]->internal_id();
case Qt::CheckStateRole:
switch (column)
{
case ActiveColumn:
return mods[row]->enabled() ? Qt::Checked : Qt::Unchecked;
return at(row)->enabled() ? Qt::Checked : Qt::Unchecked;
default:
return QVariant();
}
@ -479,61 +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
{
switch (role)
@ -573,65 +142,142 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in
return QVariant();
}
Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const
int ModFolderModel::columnCount(const QModelIndex &parent) const
{
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
auto flags = defaultFlags;
if(interaction_disabled) {
flags &= ~Qt::ItemIsDropEnabled;
}
else
{
flags |= Qt::ItemIsDropEnabled;
if(index.isValid()) {
flags |= Qt::ItemIsUserCheckable;
return NUM_COLUMNS;
}
Task* ModFolderModel::createUpdateTask()
{
auto index_dir = indexDir();
auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load);
m_first_folder_load = false;
return task;
}
Task* ModFolderModel::createParseTask(Resource& resource)
{
return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo());
}
bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
{
for(auto mod : allMods()){
if(mod->fileinfo().fileName() == filename){
auto index_dir = indexDir();
mod->destroy(index_dir, preserve_metadata);
update();
return true;
}
}
return flags;
return false;
}
Qt::DropActions ModFolderModel::supportedDropActions() const
bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
{
// copy from outside, move from within and other mod lists
return Qt::CopyAction | Qt::MoveAction;
}
QStringList ModFolderModel::mimeTypes() const
{
QStringList types;
types << "text/uri-list";
return types;
}
bool ModFolderModel::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()))
{
if(!m_can_interact) {
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
installMod(url.toLocalFile());
}
if(indexes.isEmpty())
return true;
for (auto i: indexes)
{
if(i.column() != 0) {
continue;
}
auto m = at(i.row());
auto index_dir = indexDir();
m->destroy(index_dir);
}
return false;
update();
return true;
}
bool ModFolderModel::isValid()
{
return m_dir.exists() && m_dir.isReadable();
}
bool ModFolderModel::startWatching()
{
// Remove orphaned metadata next time
m_first_folder_load = true;
return ResourceFolderModel::startWatching({ m_dir.absolutePath(), indexDir().absolutePath() });
}
bool ModFolderModel::stopWatching()
{
return ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() });
}
auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod*>
{
QList<Mod*> selected_resources;
for (auto i : indexes) {
if(i.column() != 0)
continue;
selected_resources.push_back(at(i.row()));
}
return selected_resources;
}
auto ModFolderModel::allMods() -> QList<Mod*>
{
QList<Mod*> mods;
for (auto& res : 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 "Mod.h"
#include "ResourceFolderModel.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalModParseTask.h"
@ -56,7 +57,7 @@ class QFileSystemWatcher;
* A legacy mod list.
* Backed by a folder.
*/
class ModFolderModel : public QAbstractListModel
class ModFolderModel : public ResourceFolderModel
{
Q_OBJECT
public:
@ -75,105 +76,38 @@ public:
};
ModFolderModel(const QString &dir, bool is_indexed = false);
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
Qt::DropActions supportedDropActions() const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/// flags, mostly to support drag&drop
virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
QStringList mimeTypes() const override;
bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
int columnCount(const QModelIndex &parent) const override;
virtual int rowCount(const QModelIndex &) const override
{
return size();
}
virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
virtual int columnCount(const QModelIndex &parent) const override;
size_t size() const
{
return mods.size();
}
;
bool empty() const
{
return size() == 0;
}
Mod& operator[](size_t index)
{
return *mods[index];
}
const Mod& at(size_t index) const
{
return *mods.at(index);
}
/// Reloads the mod list and returns true if the list changed.
bool update();
/**
* Adds the given mod to the list at the given index - if the list supports custom ordering
*/
bool installMod(const QString& filename);
[[nodiscard]] Task* createUpdateTask() override;
[[nodiscard]] Task* createParseTask(Resource&) override;
bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); }
bool uninstallMod(const QString& filename, bool preserve_metadata = false);
/// Deletes all the selected mods
bool deleteMods(const QModelIndexList &indexes);
/// Enable or disable listed mods
bool setModStatus(const QModelIndexList &indexes, ModStatusAction action);
void startWatching();
void stopWatching();
bool isValid();
QDir& dir()
{
return m_dir;
}
bool startWatching() override;
bool stopWatching() override;
QDir indexDir()
{
return { QString("%1/.index").arg(dir().absolutePath()) };
}
QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; }
const QList<Mod::Ptr>& allMods()
{
return mods;
}
auto selectedMods(QModelIndexList& indexes) -> QList<Mod*>;
auto allMods() -> QList<Mod*>;
auto selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>;
public slots:
void disableInteraction(bool disabled);
RESOURCE_HELPERS(Mod)
private
slots:
void directoryChanged(QString path);
void finishUpdate();
void finishModParse(int token);
signals:
void updateFinished();
private:
void resolveMod(Mod::Ptr m);
bool setModStatus(int index, ModStatusAction action);
void onUpdateSucceeded() override;
void onParseSucceeded(int ticket, QString resource_id) override;
protected:
QFileSystemWatcher *m_watcher;
bool is_watching = false;
ModFolderLoadTask::ResultPtr m_update;
bool scheduled_update = false;
bool interaction_disabled = false;
QDir m_dir;
bool m_is_indexed;
QMap<QString, int> modsIndex;
QMap<int, LocalModParseTask::ResultPtr> activeTickets;
int nextResolutionTicket = 0;
QList<Mod::Ptr> mods;
bool m_first_folder_load = true;
};

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,58 +1,151 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ResourcePackFolderModel.h"
ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) {
#include "Version.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir))
{
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE };
}
QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (role == Qt::ToolTipRole) {
switch (section) {
case ActiveColumn:
return tr("Is the resource pack enabled?");
case NameColumn:
return tr("The name of the resource pack.");
case VersionColumn:
return tr("The version of the resource pack.");
case DateColumn:
return tr("The date and time this resource pack was last changed (or added).");
default:
return QVariant();
QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
{
if (!validateIndex(index))
return {};
int row = index.row();
int column = index.column();
switch (role) {
case Qt::DisplayRole:
switch (column) {
case NameColumn:
return m_resources[row]->name();
case PackFormatColumn: {
auto resource = at(row);
auto pack_format = resource->packFormat();
if (pack_format == 0)
return tr("Unrecognized");
auto version_bounds = resource->compatibleVersions();
if (version_bounds.first.toString().isEmpty())
return QString::number(pack_format);
return QString("%1 (%2 - %3)")
.arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString());
}
case DateColumn:
return m_resources[row]->dateTimeChanged();
default:
return {};
}
case Qt::ToolTipRole: {
if (column == PackFormatColumn) {
//: The string being explained by this is in the format: ID (Lower version - Upper version)
return tr("The resource pack format ID, as well as the Minecraft versions it was designed for.");
}
return m_resources[row]->internal_id();
}
case Qt::CheckStateRole:
switch (column) {
case ActiveColumn:
return at(row)->enabled() ? Qt::Checked : Qt::Unchecked;
default:
return {};
}
default:
return {};
}
return ModFolderModel::headerData(section, orientation, role);
}
QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
{
switch (role) {
case Qt::DisplayRole:
switch (section) {
case ActiveColumn:
return QString();
case NameColumn:
return tr("Name");
case PackFormatColumn:
return tr("Pack Format");
case DateColumn:
return tr("Last changed");
default:
return {};
}
case Qt::ToolTipRole:
switch (section) {
case ActiveColumn:
return tr("Is the resource pack enabled? (Only valid for ZIPs)");
case NameColumn:
return tr("The name of the resource pack.");
case PackFormatColumn:
//: The string being explained by this is in the format: ID (Lower version - Upper version)
return tr("The resource pack format ID, as well as the Minecraft versions it was designed for.");
case DateColumn:
return tr("The date and time this resource pack was last changed (or added).");
default:
return {};
}
default:
return {};
}
return {};
}
int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const
{
return NUM_COLUMNS;
}
Task* ResourcePackFolderModel::createUpdateTask()
{
return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return new ResourcePack(entry); });
}
Task* ResourcePackFolderModel::createParseTask(Resource& resource)
{
return new LocalResourcePackParseTask(m_next_resolution_ticket, static_cast<ResourcePack&>(resource));
}

View File

@ -1,13 +1,31 @@
#pragma once
#include "ModFolderModel.h"
#include "ResourceFolderModel.h"
class ResourcePackFolderModel : public ModFolderModel
#include "ResourcePack.h"
class ResourcePackFolderModel : public ResourceFolderModel
{
Q_OBJECT
public:
enum Columns
{
ActiveColumn = 0,
NameColumn,
PackFormatColumn,
DateColumn,
NUM_COLUMNS
};
explicit ResourcePackFolderModel(const QString &dir);
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
[[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
[[nodiscard]] int columnCount(const QModelIndex &parent) const override;
[[nodiscard]] Task* createUpdateTask() override;
[[nodiscard]] Task* createParseTask(Resource&) override;
RESOURCE_HELPERS(ResourcePack)
};

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,58 +1,52 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "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 {
if (role == Qt::ToolTipRole) {
switch (section) {
case ActiveColumn:
return tr("Is the texture pack enabled?");
case NameColumn:
return tr("The name of the texture pack.");
case VersionColumn:
return tr("The version of the texture pack.");
case DateColumn:
return tr("The date and time this texture pack was last changed (or added).");
default:
return QVariant();
}
}
return ModFolderModel::headerData(section, orientation, role);
Task* TexturePackFolderModel::createParseTask(Resource& resource)
{
return new LocalTexturePackParseTask(m_next_resolution_ticket, static_cast<TexturePack&>(resource));
}

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

View File

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

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