Merge branch 'develop' into data-packs

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad 2024-08-22 20:00:49 +01:00 committed by GitHub
commit e2f3641395
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
516 changed files with 10962 additions and 7457 deletions

View File

@ -1,4 +1,5 @@
Checks:
- modernize-use-using
- readability-avoid-const-params-in-decls
SystemHeaders: false

View File

@ -1,41 +0,0 @@
#!/usr/bin/env bash
URL_JDK8="https://api.adoptium.net/v3/binary/version/jdk8u312-b07/linux/x64/jre/hotspot/normal/eclipse"
URL_JDK17="https://api.adoptium.net/v3/binary/latest/17/ga/linux/x64/jre/hotspot/normal/eclipse"
mkdir -p JREs
pushd JREs
wget --content-disposition "$URL_JDK8"
wget --content-disposition "$URL_JDK17"
for file in *;
do
mkdir temp
re='(OpenJDK([[:digit:]]+)U-jre_x64_linux_hotspot_([[:digit:]]+)(.*).tar.gz)'
if [[ $file =~ $re ]];
then
version_major=${BASH_REMATCH[2]}
version_trailing=${BASH_REMATCH[4]}
if [ $version_major = 17 ];
then
hyphen='-'
else
hyphen=''
fi
version_edit=$(echo $version_trailing | sed -e 's/_/+/g' | sed -e 's/b/-b/g')
dir_name=jdk$hyphen$version_major$version_edit-jre
mkdir jre$version_major
tar -xzf $file -C temp
pushd temp/$dir_name
cp -r . ../../jre$version_major
popd
fi
rm -rf temp
done
popd

View File

@ -16,6 +16,7 @@ jobs:
permissions:
contents: write # for korthout/backport-action to create branch
pull-requests: write # for korthout/backport-action to create PR to backport
actions: write # for korthout/backport-action to create PR with workflow changes
name: Backport Pull Request
if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name))
runs-on: ubuntu-latest
@ -24,7 +25,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs
uses: korthout/backport-action@v2.1.1
uses: korthout/backport-action@v3.0.2
with:
# Config README: https://github.com/korthout/backport-action#backport-action
pull_description: |-

View File

@ -21,8 +21,23 @@ on:
WINDOWS_CODESIGN_PASSWORD:
description: Password for signing Windows builds
required: false
CACHIX_AUTH_TOKEN:
description: Private token for authenticating against Cachix cache
APPLE_CODESIGN_CERT:
description: Certificate for signing macOS builds
required: false
APPLE_CODESIGN_PASSWORD:
description: Password for signing macOS builds
required: false
APPLE_CODESIGN_ID:
description: Certificate ID for signing macOS builds
required: false
APPLE_NOTARIZE_APPLE_ID:
description: Apple ID used for notarizing macOS builds
required: false
APPLE_NOTARIZE_TEAM_ID:
description: Team ID used for notarizing macOS builds
required: false
APPLE_NOTARIZE_PASSWORD:
description: Password used for notarizing macOS builds
required: false
GPG_PRIVATE_KEY:
description: Private key for AppImage signing
@ -39,14 +54,17 @@ jobs:
include:
- os: ubuntu-20.04
qt_ver: 5
qt_host: linux
qt_arch: ""
qt_version: "5.12.8"
qt_modules: "qtnetworkauth"
- os: ubuntu-20.04
qt_ver: 6
qt_host: linux
qt_arch: ""
qt_version: "6.2.4"
qt_modules: "qt5compat qtimageformats"
qt_tools: ""
qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: windows-2022
name: "Windows-MinGW-w64"
@ -60,10 +78,9 @@ jobs:
vcvars_arch: "amd64"
qt_ver: 6
qt_host: windows
qt_arch: ''
qt_version: '6.6.0'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
qt_arch: ""
qt_version: "6.7.2"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: windows-2022
name: "Windows-MSVC-arm64"
@ -72,20 +89,18 @@ jobs:
vcvars_arch: "amd64_arm64"
qt_ver: 6
qt_host: windows
qt_arch: 'win64_msvc2019_arm64'
qt_version: '6.6.0'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
qt_arch: "win64_msvc2019_arm64"
qt_version: "6.7.2"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: macos-12
name: macOS
macosx_deployment_target: 11.0
qt_ver: 6
qt_host: mac
qt_arch: ''
qt_version: '6.6.0'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
qt_arch: ""
qt_version: "6.7.2"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: macos-12
name: macOS-Legacy
@ -93,8 +108,7 @@ jobs:
qt_ver: 5
qt_host: mac
qt_version: "5.15.2"
qt_modules: ""
qt_tools: ""
qt_modules: "qtnetworkauth"
runs-on: ${{ matrix.os }}
@ -136,6 +150,7 @@ jobs:
quazip-qt6:p
ccache:p
qt6-5compat:p
qt6-networkauth:p
cmark:p
- name: Force newer ccache
@ -145,13 +160,13 @@ jobs:
- name: Setup ccache
if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug'
uses: hendrikmuhs/ccache-action@v1.2.10
uses: hendrikmuhs/ccache-action@v1.2.14
with:
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
- name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
uses: actions/cache@v3.3.2
uses: actions/cache@v4.0.2
with:
path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
@ -192,11 +207,6 @@ jobs:
brew update
brew install ninja extra-cmake-modules
- name: Install Qt (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 6
run: |
sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5
- name: Install host Qt (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
uses: jurplel/install-qt-action@v3
@ -208,20 +218,18 @@ jobs:
target: "desktop"
arch: ""
modules: ${{ matrix.qt_modules }}
tools: ${{ matrix.qt_tools }}
cache: ${{ inputs.is_qt_cached }}
cache-key-prefix: host-qt-arm64-windows
dir: ${{ github.workspace }}\HostQt
set-env: false
- name: Install Qt (macOS, Linux, Qt 6 & Windows MSVC)
if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS' || (runner.os == 'Windows' && matrix.msystem == '')
- name: Install Qt (macOS, Linux & Windows MSVC)
if: matrix.msystem == ''
uses: jurplel/install-qt-action@v3
with:
aqtversion: "==3.1.*"
py7zrversion: ">=0.20.2"
version: ${{ matrix.qt_version }}
host: ${{ matrix.qt_host }}
target: "desktop"
arch: ${{ matrix.qt_arch }}
modules: ${{ matrix.qt_modules }}
@ -244,7 +252,6 @@ jobs:
wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage"
${{ github.workspace }}/.github/scripts/prepare_JREs.sh
sudo apt install libopengl0
- name: Add QT_HOST_PATH var (Windows MSVC arm64)
@ -336,6 +343,20 @@ jobs:
# PACKAGE BUILDS
##
- name: Fetch codesign certificate (macOS)
if: runner.os == 'macOS'
run: |
echo '${{ secrets.APPLE_CODESIGN_CERT }}' | base64 --decode > codesign.p12
if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then
security create-keychain -p '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain
security import codesign.p12 -k build.keychain -P '${{ secrets.APPLE_CODESIGN_PASSWORD }}' -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain
else
echo ":warning: Using ad-hoc code signing for macOS, as certificate was not present." >> $GITHUB_STEP_SUMMARY
fi
- name: Package (macOS)
if: runner.os == 'macOS'
run: |
@ -343,9 +364,34 @@ jobs:
cd ${{ env.INSTALL_DIR }}
chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher"
sudo codesign --sign - --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then
APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}'
else
APPLE_CODESIGN_ID='-'
fi
sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
mv "PrismLauncher.app" "Prism Launcher.app"
tar -czf ../PrismLauncher.tar.gz *
- name: Notarize (macOS)
if: runner.os == 'macOS'
run: |
cd ${{ env.INSTALL_DIR }}
if [ -n '${{ secrets.APPLE_NOTARIZE_PASSWORD }}' ]; then
ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip
xcrun notarytool submit ../PrismLauncher.zip \
--wait --progress \
--apple-id '${{ secrets.APPLE_NOTARIZE_APPLE_ID }}' \
--team-id '${{ secrets.APPLE_NOTARIZE_TEAM_ID }}' \
--password '${{ secrets.APPLE_NOTARIZE_PASSWORD }}'
xcrun stapler staple "Prism Launcher.app"
else
echo ":warning: Skipping notarization as credentials are not present." >> $GITHUB_STEP_SUMMARY
fi
ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip
- name: Make Sparkle signature (macOS)
if: matrix.name == 'macOS'
@ -353,7 +399,7 @@ jobs:
if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then
brew install openssl@3
echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem
signature=$(/usr/local/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.tar.gz -inkey ed25519-priv.pem | openssl base64 | tr -d \\n)
signature=$(/usr/local/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n)
rm ed25519-priv.pem
cat >> $GITHUB_STEP_SUMMARY << EOF
### Artifact Information :information_source:
@ -379,12 +425,6 @@ jobs:
run: |
cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }}
cd ${{ env.INSTALL_DIR }}
if ("${{ matrix.qt_ver }}" -eq "5")
{
Copy-Item ${{ runner.workspace }}/Qt/Tools/OpenSSL/Win_x86/bin/libcrypto-1_1.dll -Destination libcrypto-1_1.dll
Copy-Item ${{ runner.workspace }}/Qt/Tools/OpenSSL/Win_x86/bin/libssl-1_1.dll -Destination libssl-1_1.dll
}
cd ${{ github.workspace }}
Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
@ -437,26 +477,6 @@ jobs:
":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
}
- name: Package (Linux)
if: runner.os == 'Linux'
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_DIR }}
for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_DIR }}/manifest.txt
cd ${{ env.INSTALL_DIR }}
tar --owner root --group root -czf ../PrismLauncher.tar.gz *
- name: Package (Linux, portable)
if: runner.os == 'Linux'
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }}
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
cd ${{ env.INSTALL_PORTABLE_DIR }}
tar -czf ../PrismLauncher-portable.tar.gz *
- name: Package AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5
shell: bash
@ -472,13 +492,9 @@ jobs:
chmod +x linuxdeploy-*.AppImage
mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-{8,17}-openjdk
mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib
mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
cp -r ${{ github.workspace }}/JREs/jre8/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk
cp -r ${{ github.workspace }}/JREs/jre17/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk
cp -r ${{ runner.workspace }}/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/
@ -486,10 +502,6 @@ jobs:
cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server"
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64"
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk/lib/server"
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk/lib"
export LD_LIBRARY_PATH
chmod +x AppImageUpdate-x86_64.AppImage
@ -511,76 +523,81 @@ jobs:
mv "PrismLauncher-Linux-x86_64.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage"
- name: Package (Linux, portable)
if: runner.os == 'Linux'
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_PORTABLE_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -DINSTALL_BUNDLE=full -G Ninja
cmake --install ${{ env.BUILD_DIR }}
cmake --install ${{ env.BUILD_DIR }} --component portable
mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libffi.so.7 ${{ env.INSTALL_PORTABLE_DIR }}/lib
mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
cd ${{ env.INSTALL_PORTABLE_DIR }}
tar -czf ../PrismLauncher-portable.tar.gz *
##
# UPLOAD BUILDS
##
- name: Upload binary tarball (macOS)
if: runner.os == 'macOS'
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher.tar.gz
path: PrismLauncher.zip
- name: Upload binary zip (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
path: ${{ env.INSTALL_DIR }}/**
- name: Upload binary zip (Windows, portable)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ matrix.name }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
path: ${{ env.INSTALL_PORTABLE_DIR }}/**
- name: Upload installer (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ matrix.name }}-Setup-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher-Setup.exe
- name: Upload binary tarball (Linux, Qt 5)
if: runner.os == 'Linux' && matrix.qt_ver != 6
uses: actions/upload-artifact@v3
with:
name: PrismLauncher-${{ runner.os }}-Qt5-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher.tar.gz
- name: Upload binary tarball (Linux, portable, Qt 5)
if: runner.os == 'Linux' && matrix.qt_ver != 6
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ runner.os }}-Qt5-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher-portable.tar.gz
- name: Upload binary tarball (Linux, Qt 6)
if: runner.os == 'Linux' && matrix.qt_ver !=5
uses: actions/upload-artifact@v3
with:
name: PrismLauncher-${{ runner.os }}-Qt6-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher.tar.gz
- name: Upload binary tarball (Linux, portable, Qt 6)
if: runner.os == 'Linux' && matrix.qt_ver != 5
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ runner.os }}-Qt6-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher-portable.tar.gz
- name: Upload AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
- name: Upload AppImage Zsync (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage.zsync
path: PrismLauncher-Linux-x86_64.AppImage.zsync
@ -594,7 +611,7 @@ jobs:
flatpak:
runs-on: ubuntu-latest
container:
image: bilelmoussaoui/flatpak-github-actions:kde-5.15-22.08
image: bilelmoussaoui/flatpak-github-actions:kde-6.7
options: --privileged
steps:
- name: Checkout

View File

@ -13,7 +13,7 @@ jobs:
submodules: 'true'
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
config-file: ./.github/codeql/codeql-config.yml
queries: security-and-quality
@ -23,7 +23,7 @@ jobs:
run:
sudo apt-get -y update
sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5
sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 libqt5networkauth5 libqt5networkauth5-dev
- name: Configure and Build
run: |
@ -32,4 +32,4 @@ jobs:
cmake --build build
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3

View File

@ -32,6 +32,11 @@ jobs:
SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }}
APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }}
APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }}
APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}

View File

@ -16,7 +16,12 @@ jobs:
SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }}
APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }}
APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }}
APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}
@ -32,7 +37,7 @@ jobs:
submodules: "true"
path: "PrismLauncher-source"
- name: Download artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
- name: Grab and store version
run: |
tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$")
@ -41,13 +46,11 @@ jobs:
run: |
mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }}
mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz
mv PrismLauncher-Linux-Qt6*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz
mv PrismLauncher-Linux-Qt5-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz
mv PrismLauncher-Linux-Qt5*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt5-${{ env.VERSION }}.tar.gz
mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage
mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync
mv PrismLauncher-macOS-Legacy*/PrismLauncher.tar.gz PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz
mv PrismLauncher-macOS*/PrismLauncher.tar.gz PrismLauncher-macOS-${{ env.VERSION }}.tar.gz
mv PrismLauncher-macOS-Legacy*/PrismLauncher.zip PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip
mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip
tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}
@ -79,7 +82,7 @@ jobs:
- name: Create release
id: create_release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ github.ref }}
@ -87,11 +90,9 @@ jobs:
draft: true
prerelease: false
files: |
PrismLauncher-Linux-Qt5-${{ env.VERSION }}.tar.gz
PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz
PrismLauncher-Linux-x86_64.AppImage
PrismLauncher-Linux-x86_64.AppImage.zsync
PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz
PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz
PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip
PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip
@ -102,6 +103,6 @@ jobs:
PrismLauncher-Windows-MSVC-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe
PrismLauncher-macOS-${{ env.VERSION }}.tar.gz
PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz
PrismLauncher-macOS-${{ env.VERSION }}.zip
PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip
PrismLauncher-${{ env.VERSION }}.tar.gz

View File

@ -17,9 +17,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@6a9a9e84a173d90b3ffb42c5ddaf9ea033fad011 # v23
- uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
- uses: DeterminateSystems/update-flake-lock@v20
- uses: DeterminateSystems/update-flake-lock@v23
with:
commit-msg: "chore(nix): update lockfile"
pr-title: "chore(nix): update lockfile"

View File

@ -1,3 +0,0 @@
# Build Instructions
Full build instructions are available on [the website](https://prismlauncher.org/wiki/development/build-instructions/).

View File

@ -178,7 +178,7 @@ set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL th
set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help")
######## Set version numbers ########
set(Launcher_VERSION_MAJOR 8)
set(Launcher_VERSION_MAJOR 9)
set(Launcher_VERSION_MINOR 0)
set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
@ -282,7 +282,7 @@ endif()
include(QtVersionlessBackport)
if(Launcher_QT_VERSION_MAJOR EQUAL 5)
set(QT_VERSION_MAJOR 5)
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml)
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml NetworkAuth)
if(NOT Launcher_FORCE_BUNDLED_LIBS)
find_package(QuaZip-Qt5 1.3 QUIET)
@ -296,7 +296,7 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 5)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE")
elseif(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_VERSION_MAJOR 6)
find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat)
find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth)
list(APPEND Launcher_QT_LIBS Qt6::Core5Compat)
if(NOT Launcher_FORCE_BUNDLED_LIBS)
@ -377,12 +377,12 @@ if(UNIX AND APPLE)
set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_NAME}")
set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_NAME}")
set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns)
set(MACOSX_BUNDLE_COPYRIGHT "© 2022-2023 ${Launcher_Copyright_Mac}")
set(MACOSX_BUNDLE_COPYRIGHT "${Launcher_Copyright_Mac}")
set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed")
set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed")
set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.1.0/Sparkle-2.1.0.tar.xz" CACHE STRING "URL to Sparkle release archive")
set(MACOSX_SPARKLE_SHA256 "bf6ac1caa9f8d321d5784859c88da874f28412f37fb327bc21b7b14c5d61ef94" CACHE STRING "SHA256 checksum for Sparkle release archive")
set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.5.2/Sparkle-2.5.2.tar.xz" CACHE STRING "URL to Sparkle release archive")
set(MACOSX_SPARKLE_SHA256 "572dd67ae398a466f19f343a449e1890bac1ef74885b4739f68f979a8a89884b" CACHE STRING "SHA256 checksum for Sparkle release archive")
set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle")
# directories to look for dependencies
@ -417,7 +417,19 @@ elseif(UNIX)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR})
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}")
if (INSTALL_BUNDLE STREQUAL full)
set(PLUGIN_DEST_DIR "plugins")
set(BUNDLE_DEST_DIR ".")
set(RESOURCES_DEST_DIR ".")
# Apps to bundle
set(APPS "\${CMAKE_INSTALL_PREFIX}/bin/${Launcher_APP_BINARY_NAME}")
# directories to look for dependencies
set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
endif()
if(Launcher_ManPage)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6")
endif()
@ -504,15 +516,13 @@ else()
endif()
if(NOT cmark_FOUND)
message(STATUS "Using bundled cmark")
set(CMARK_STATIC ON CACHE BOOL "Build static libcmark library" FORCE)
set(CMARK_SHARED OFF CACHE BOOL "Build shared libcmark library" FORCE)
set(CMARK_TESTS OFF CACHE BOOL "Build cmark tests and enable testing" FORCE)
set(BUILD_TESTING 0)
set(BUILD_SHARED_LIBS 0)
add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser
add_library(cmark::cmark ALIAS cmark_static)
add_library(cmark::cmark ALIAS cmark)
else()
message(STATUS "Using system cmark")
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)

View File

@ -1,7 +1,7 @@
## Prism Launcher
Prism Launcher - Minecraft Launcher
Copyright (C) 2022-2023 Prism Launcher Contributors
Copyright (C) 2022-2024 Prism Launcher Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -333,32 +333,6 @@
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)
Copyright (c) 2012, Akos Polster
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.
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 HOLDER 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.
## Gamemode
Copyright (c) 2017-2022, Feral Interactive
@ -436,7 +410,7 @@
Copyright (C) 2007 Johann Ollivier Lapeyre <johann@oxygen-icons.org>
Copyright (C) 2007 Kenneth Wimer <kwwii@bootsplash.org>
Copyright (C) 2007 Riccardo Iaconelli <riccardo@oxygen-icons.org>
and others
This library is free software; you can redistribute it and/or

View File

@ -61,7 +61,12 @@ The translation effort for Prism Launcher is hosted on [Weblate](https://hosted.
## Building
If you want to build Prism Launcher yourself, check the [Build Instructions](https://prismlauncher.org/wiki/development/build-instructions/).
If you want to build Prism Launcher yourself, check the build instructions:
- [Windows](https://prismlauncher.org/wiki/development/build-instructions/windows/)
- [Linux](https://prismlauncher.org/wiki/development/build-instructions/linux/)
- [MacOS](https://prismlauncher.org/wiki/development/build-instructions/macos/)
- [OpenBSD](https://prismlauncher.org/wiki/development/build-instructions/openbsd/)
## Sponsors & Partners

View File

@ -163,7 +163,6 @@ class Config {
QString RESOURCE_BASE = "https://resources.download.minecraft.net/";
QString LIBRARY_BASE = "https://libraries.minecraft.net/";
QString AUTH_BASE = "https://authserver.mojang.com/";
QString IMGUR_BASE_URL = "https://api.imgur.com/3/";
QString FMLLIBS_BASE_URL = "https://files.prismlauncher.org/fmllibs/"; // FIXME: move into CMakeLists
QString TRANSLATIONS_BASE_URL = "https://i18n.prismlauncher.org/"; // FIXME: move into CMakeLists

View File

@ -68,6 +68,8 @@ function(
/w14906 # string literal cast to 'LPWSTR'
/w14928 # illegal copy-initialization; more than one user-defined conversion has been implicitly applied
/permissive- # standards conformance mode for MSVC compiler.
/we4062 # forbid omitting a possible value of an enum in a switch statement
)
endif()
@ -93,6 +95,8 @@ function(
# in a lot of noise. This warning is only notifying us that clang is emulating the GCC behaviour
# instead of the exact standard wording so we can safely ignore it
-Wno-gnu-zero-variadic-macro-arguments
-Werror=switch # forbid omitting a possible value of an enum in a switch statement
)
endif()
@ -104,6 +108,8 @@ function(
-Wduplicated-branches # warn if if / else branches have duplicated code
-Wlogical-op # warn about logical operations being used where bitwise were probably wanted
-Wuseless-cast # warn if you perform a cast to the same type
-Werror=switch # forbid omitting a possible value of an enum in a switch statement
)
endif()
@ -128,6 +134,8 @@ function(
-Woverloaded-virtual
-Wuseless-cast
-Wextra-semi
-Werror=switch # forbid omitting a possible value of an enum in a switch statement
)
target_compile_options(

View File

@ -6,6 +6,8 @@
<string>A Minecraft mod wants to access your camera.</string>
<key>NSMicrophoneUsageDescription</key>
<string>A Minecraft mod wants to access your microphone.</string>
<key>NSDownloadsFolderUsageDescription</key>
<string>Prism uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where Prism scans for downloaded mods in Settings or the prompt that appears.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHighResolutionCapable</key>
@ -77,6 +79,14 @@
<string>curseforge</string>
</array>
</dict>
<dict>
<key>CFBundleURLName</key>
<string>Prismlauncher</string>
<key>CFBundleURLSchemes</key>
<array>
<string>prismlauncher</string>
</array>
</dict>
</array>
</dict>
</plist>

96
flake.lock generated
View File

@ -18,14 +18,16 @@
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
"nixpkgs-lib": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1698882062,
"narHash": "sha256-HkhafUayIqxXyHH1X8d9RDl1M2CkFgZLjKD3MzabiEo=",
"lastModified": 1722555600,
"narHash": "sha256-XOQkdLafnb/p9ij77byFQjDf5m5QYl9b2REiVClC+x4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "8c9fa2545007b49a5db5f650ae91f227672c3877",
"rev": "8471fe90ad337a8074e957b69ca4d0089218391d",
"type": "github"
},
"original": {
@ -34,24 +36,6 @@
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1685518550,
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
@ -60,11 +44,11 @@
]
},
"locked": {
"lastModified": 1660459072,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
@ -89,49 +73,16 @@
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1694857738,
"narHash": "sha256-bxxNyLHjhu0N8T3REINXQ2ZkJco0ABFPn6PIe2QUfqo=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "41fd48e00c22b4ced525af521ead8792402de0ea",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-filter",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1699343069,
"narHash": "sha256-s7BBhyLA6MI6FuJgs4F/SgpntHBzz40/qV0xLPW6A1Q=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "ec750fd01963ab6b20ee1f0cb488754e8036d89d",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1698611440,
"narHash": "sha256-jPjHjrerhYDy3q9+s5EAsuhyhuknNfowY6yt6pjn9pc=",
"lastModified": 1723637854,
"narHash": "sha256-med8+5DSWa2UnOqtdICndjDAEjxr5D7zaIiK4pn0Q7c=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0cbe9f69c234a7700596e943bfae7ef27a31b735",
"rev": "c3aa7b8938b17aebd2deecf7be0636000d62a2b9",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
@ -143,7 +94,6 @@
"flake-compat": [
"flake-compat"
],
"flake-utils": "flake-utils",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
@ -153,11 +103,11 @@
]
},
"locked": {
"lastModified": 1699271226,
"narHash": "sha256-8Jt1KW3xTjolD6c6OjJm9USx/jmL+VVmbooADCkdDfU=",
"lastModified": 1723803910,
"narHash": "sha256-yezvUuFiEnCFbGuwj/bQcqg7RykIEqudOy/RBrId0pc=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "ea758da1a6dcde6dc36db348ed690d09b9864128",
"rev": "bfef0ada09e2c8ac55bbcd0831bd0c9d42e651ba",
"type": "github"
},
"original": {
@ -171,25 +121,9 @@
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"libnbtplusplus": "libnbtplusplus",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@ -1,15 +1,24 @@
{
description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of MultiMC)";
nixConfig = {
extra-substituters = ["https://cache.garnix.io"];
extra-trusted-public-keys = ["cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g="];
};
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
nix-filter.url = "github:numtide/nix-filter";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-parts = {
url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs";
};
pre-commit-hooks = {
url = "github:cachix/pre-commit-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs";
inputs.nixpkgs-stable.follows = "nixpkgs";
inputs.flake-compat.follows = "flake-compat";
inputs = {
nixpkgs.follows = "nixpkgs";
nixpkgs-stable.follows = "nixpkgs";
flake-compat.follows = "flake-compat";
};
};
flake-compat = {
url = "github:edolstra/flake-compat";

View File

@ -1,8 +1,9 @@
id: org.prismlauncher.PrismLauncher
runtime: org.kde.Platform
runtime-version: "5.15-22.08"
runtime-version: 6.7
sdk: org.kde.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.openjdk21
- org.freedesktop.Sdk.Extension.openjdk17
- org.freedesktop.Sdk.Extension.openjdk8
@ -37,7 +38,6 @@ modules:
config-opts:
- -DLauncher_BUILD_PLATFORM=flatpak
- -DCMAKE_BUILD_TYPE=RelWithDebInfo
- -DLauncher_QT_VERSION_MAJOR=5
build-options:
env:
JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17
@ -50,6 +50,8 @@ modules:
buildsystem: simple
build-commands:
- mkdir -p /app/jdk/
- /usr/lib/sdk/openjdk21/install.sh
- mv /app/jre /app/jdk/21
- /usr/lib/sdk/openjdk17/install.sh
- mv /app/jre /app/jdk/17
- /usr/lib/sdk/openjdk8/install.sh
@ -62,7 +64,8 @@ modules:
config-opts:
- -DCMAKE_BUILD_TYPE=RelWithDebInfo
- -DBUILD_SHARED_LIBS:BOOL=ON
- -DGLFW_USE_WAYLAND=ON
- -DGLFW_USE_WAYLAND:BOOL=ON
- -DGLFW_BUILD_DOCS:BOOL=OFF
sources:
- type: git
url: https://github.com/glfw/glfw.git
@ -104,9 +107,9 @@ modules:
- install -Dm755 ../data/gamemoderun -t /app/bin
sources:
- type: archive
archive-type: tar-gzip
url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.7
sha256: 57ce73ba605d1cf12f8d13725006a895182308d93eba0f69f285648449641803
dest-filename: gamemode.tar.gz
url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.8.1
sha256: 969cf85b5ca3944f3e315cd73a0ee9bea4f9c968cd7d485e9f4745bc1e679c4e
x-checker-data:
type: json
url: https://api.github.com/repos/FeralInteractive/gamemode/releases/latest

@ -1 +1 @@
Subproject commit 45094ca570be383d06df729b6972830ec63bd3df
Subproject commit f2b0c16a2a217a1822ce5a6538ba8f755ed1dd32

View File

@ -1,5 +1,6 @@
builds:
exclude: []
exclude:
- "*.x86_64-darwin.*"
include:
- "checks.x86_64-linux.*"
- "devShells.*.*"

View File

@ -47,7 +47,7 @@
#include "net/PasteUpload.h"
#include "pathmatcher/MultiMatcher.h"
#include "pathmatcher/SimplePrefixMatcher.h"
#include "settings/INIFile.h"
#include "tools/GenericProfiler.h"
#include "ui/InstanceWindow.h"
#include "ui/MainWindow.h"
@ -105,7 +105,7 @@
#include "icons/IconList.h"
#include "net/HttpMetaCache.h"
#include "java/JavaUtils.h"
#include "java/JavaInstallList.h"
#include "updater/ExternalUpdater.h"
@ -132,6 +132,15 @@
#include "gamemode_client.h"
#endif
#if defined(Q_OS_LINUX)
#include <sys/statvfs.h>
#endif
#if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
#include <sys/mount.h>
#include <sys/types.h>
#endif
#if defined(Q_OS_MAC)
#if defined(SPARKLE_ENABLED)
#include "updater/MacSparkleUpdater.h"
@ -141,6 +150,7 @@
#endif
#if defined Q_OS_WIN32
#include <windows.h>
#include "WindowsConsole.h"
#endif
@ -216,6 +226,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// Don't quit on hiding the last window
this->setQuitOnLastWindowClosed(false);
this->setQuitLockEnabled(false);
// Commandline parsing
QCommandLineParser parser;
@ -225,6 +236,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
{ { { "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" },
{ { "w", "world" }, "Join the specified world on launch (only valid in combination with --launch)", "world" },
{ { "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 or resource from specified local path or URL", "url" },
@ -239,6 +251,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_instanceIdToLaunch = parser.value("launch");
m_serverToJoin = parser.value("server");
m_worldToJoin = parser.value("world");
m_profileToUse = parser.value("profile");
m_liveCheck = parser.isSet("alive");
@ -254,7 +267,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
}
// error if --launch is missing with --server or --profile
if ((!m_serverToJoin.isEmpty() || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty()) {
if (((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty()) || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty()) {
std::cerr << "--server and --profile can only be used in combination with --launch!" << std::endl;
m_status = Application::Failed;
return;
@ -281,12 +294,17 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
QString adjustedBy;
QString dataPath;
// change folder
QString dataDirEnv;
QString dirParam = parser.value("dir");
if (!dirParam.isEmpty()) {
// the dir param. it makes multimc data path point to whatever the user specified
// on command line
adjustedBy = "Command line";
dataPath = dirParam;
} else if (dataDirEnv = QProcessEnvironment::systemEnvironment().value(QString("%1_DATA_DIR").arg(BuildConfig.LAUNCHER_NAME.toUpper()));
!dataDirEnv.isEmpty()) {
adjustedBy = "System environment";
dataPath = dataDirEnv;
} else {
QDir foo;
if (DesktopServices::isSnap()) {
@ -299,7 +317,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
adjustedBy = "Persistent data path";
#ifndef Q_OS_MACOS
if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
if (auto portableUserData = FS::PathCombine(m_rootPath, "UserData"); QDir(portableUserData).exists()) {
dataPath = portableUserData;
adjustedBy = "Portable user data path";
m_portable = true;
} else if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
dataPath = m_rootPath;
adjustedBy = "Portable data path";
m_portable = true;
@ -365,6 +387,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
if (!m_serverToJoin.isEmpty()) {
launch.args["server"] = m_serverToJoin;
} else if (!m_worldToJoin.isEmpty()) {
launch.args["world"] = m_worldToJoin;
}
if (!m_profileToUse.isEmpty()) {
launch.args["profile"] = m_profileToUse;
@ -380,20 +404,15 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
{
static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log";
static const QString logBase = FS::PathCombine("logs", baseLogFile);
auto moveFile = [](const QString& oldName, const QString& newName) {
QFile::remove(newName);
QFile::copy(oldName, newName);
QFile::remove(oldName);
};
if (FS::ensureFolderPathExists("logs")) { // if this did not fail
for (auto i = 0; i <= 4; i++)
if (auto oldName = baseLogFile.arg(i);
QFile::exists(oldName)) // do not pointlessly delete new files if the old ones are not there
moveFile(oldName, logBase.arg(i));
FS::move(oldName, logBase.arg(i));
}
for (auto i = 4; i > 0; i--)
moveFile(logBase.arg(i - 1), logBase.arg(i));
FS::move(logBase.arg(i - 1), logBase.arg(i));
logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0)));
if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
@ -433,7 +452,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// search the dataPath()
// seach app data standard path
if (!foundLoggingRules && !isPortable() && dirParam.isEmpty()) {
if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) {
logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile));
if (!logRulesPath.isEmpty()) {
qDebug() << "Found" << logRulesPath << "...";
@ -485,8 +504,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
}
{
qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME) << ", (c) 2022-2023 "
<< qPrintable(QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
qDebug() << "Version : " << BuildConfig.printableVersionString();
qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM;
qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
@ -509,6 +527,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
}
if (!m_serverToJoin.isEmpty()) {
qDebug() << "Address of server to join :" << m_serverToJoin;
} else if (!m_worldToJoin.isEmpty()) {
qDebug() << "Name of the world to join :" << m_worldToJoin;
}
qDebug() << "<> Paths set.";
}
@ -545,6 +565,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("NumberOfConcurrentTasks", 10);
m_settings->registerSetting("NumberOfConcurrentDownloads", 6);
m_settings->registerSetting("NumberOfManualRetries", 1);
m_settings->registerSetting("RequestTimeout", 60);
QString defaultMonospace;
int defaultSize = 11;
@ -579,6 +601,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("IconsDir", "icons");
m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
m_settings->registerSetting("DownloadsDirWatchRecursive", false);
m_settings->registerSetting("SkinsDir", "skins");
// Editors
m_settings->registerSetting("JsonEditor", QString());
@ -631,10 +654,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("UseNativeGLFW", false);
m_settings->registerSetting("CustomGLFWPath", "");
// Peformance related options
// Performance related options
m_settings->registerSetting("EnableFeralGamemode", false);
m_settings->registerSetting("EnableMangoHud", false);
m_settings->registerSetting("UseDiscreteGpu", false);
m_settings->registerSetting("UseZink", false);
// Game time
m_settings->registerSetting("ShowGameTime", true);
@ -645,6 +669,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// Minecraft mods
m_settings->registerSetting("ModMetadataDisabled", false);
m_settings->registerSetting("ModDependenciesDisabled", false);
m_settings->registerSetting("SkipModpackUpdatePrompt", false);
// Minecraft offline player name
m_settings->registerSetting("LastOfflinePlayerName", "");
@ -658,6 +683,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// The cat
m_settings->registerSetting("TheCat", false);
m_settings->registerSetting("CatOpacity", 100);
m_settings->registerSetting("StatusBarVisible", true);
m_settings->registerSetting("ToolbarsLocked", false);
@ -745,6 +773,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("ModrinthToken", "");
m_settings->registerSetting("UserAgentOverride", "");
// FTBApp instances
m_settings->registerSetting("FTBAppInstancesPath", "");
// Init page provider
{
m_globalSettingsProvider = std::make_shared<GenericPageProvider>(tr("Settings"));
@ -798,7 +829,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_icons.reset(new IconList(instFolders, setting->get().toString()));
connect(setting.get(), &Setting::SettingChanged,
[&](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); });
qDebug() << "<> Instance icons intialized.";
qDebug() << "<> Instance icons initialized.";
}
// Themes
@ -835,24 +866,17 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
{
m_metacache.reset(new HttpMetaCache("metacache"));
m_metacache->addBase("asset_indexes", QDir("assets/indexes").absolutePath());
m_metacache->addBase("asset_objects", QDir("assets/objects").absolutePath());
m_metacache->addBase("versions", QDir("versions").absolutePath());
m_metacache->addBase("libraries", QDir("libraries").absolutePath());
m_metacache->addBase("minecraftforge", QDir("mods/minecraftforge").absolutePath());
m_metacache->addBase("fmllibs", QDir("mods/minecraftforge/libs").absolutePath());
m_metacache->addBase("liteloader", QDir("mods/liteloader").absolutePath());
m_metacache->addBase("general", QDir("cache").absolutePath());
m_metacache->addBase("ATLauncherPacks", QDir("cache/ATLauncherPacks").absolutePath());
m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath());
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("ModrinthModpacks", QDir("cache/ModrinthModpacks").absolutePath());
m_metacache->addBase("root", QDir::currentPath());
m_metacache->addBase("translations", QDir("translations").absolutePath());
m_metacache->addBase("icons", QDir("cache/icons").absolutePath());
m_metacache->addBase("meta", QDir("meta").absolutePath());
m_metacache->Load();
qDebug() << "<> Cache initialized.";
@ -864,6 +888,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// FIXME: what to do with these?
m_profilers.insert("jprofiler", std::shared_ptr<BaseProfilerFactory>(new JProfilerFactory()));
m_profilers.insert("jvisualvm", std::shared_ptr<BaseProfilerFactory>(new JVisualVMFactory()));
m_profilers.insert("generic", std::shared_ptr<BaseProfilerFactory>(new GenericProfilerFactory()));
for (auto profiler : m_profilers.values()) {
profiler->registerSettings(m_settings);
}
@ -932,8 +957,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
[[fallthrough]];
default: {
qDebug() << "Exiting because update lockfile is present";
QMetaObject::invokeMethod(
this, []() { exit(1); }, Qt::QueuedConnection);
QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection);
return;
}
}
@ -965,8 +989,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
[[fallthrough]];
default: {
qDebug() << "Exiting because update lockfile is present";
QMetaObject::invokeMethod(
this, []() { exit(1); }, Qt::QueuedConnection);
QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection);
return;
}
}
@ -978,7 +1001,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
"\n"
"You are now running %1 .\n"
"Check the Prism Launcher updater log at: \n"
"%1\n"
"%2\n"
"for details.")
.arg(BuildConfig.printableVersionString())
.arg(update_log_path);
@ -993,6 +1016,37 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
}
}
// notify user if /tmp is mounted with `noexec` (#1693)
{
bool is_tmp_noexec = false;
#if defined(Q_OS_LINUX)
struct statvfs tmp_stat;
statvfs("/tmp", &tmp_stat);
is_tmp_noexec = tmp_stat.f_flag & ST_NOEXEC;
#elif defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
struct statfs tmp_stat;
statfs("/tmp", &tmp_stat);
is_tmp_noexec = tmp_stat.f_flags & MNT_NOEXEC;
#endif
if (is_tmp_noexec) {
auto infoMsg =
tr("Your /tmp directory is currently mounted with the 'noexec' flag enabled.\n"
"Some versions of Minecraft may not launch.\n");
auto msgBox = new QMessageBox(QMessageBox::Information, tr("Incompatible system configuration"), infoMsg, QMessageBox::Ok);
msgBox->setDefaultButton(QMessageBox::Ok);
msgBox->setAttribute(Qt::WA_DeleteOnClose);
msgBox->setMinimumWidth(460);
msgBox->adjustSize();
msgBox->open();
}
}
if (createSetupWizard()) {
return;
}
@ -1114,14 +1168,17 @@ void Application::performMainStartupAction()
if (!m_instanceIdToLaunch.isEmpty()) {
auto inst = instances()->getInstanceById(m_instanceIdToLaunch);
if (inst) {
MinecraftServerTargetPtr serverToJoin = nullptr;
MinecraftTarget::Ptr targetToJoin = nullptr;
MinecraftAccountPtr accountToUse = nullptr;
qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching";
if (!m_serverToJoin.isEmpty()) {
// FIXME: validate the server string
serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(m_serverToJoin)));
targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(m_serverToJoin, false)));
qDebug() << " Launching with server" << m_serverToJoin;
} else if (!m_worldToJoin.isEmpty()) {
targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(m_worldToJoin, true)));
qDebug() << " Launching with world" << m_worldToJoin;
}
if (!m_profileToUse.isEmpty()) {
@ -1132,7 +1189,7 @@ void Application::performMainStartupAction()
qDebug() << " Launching with account" << m_profileToUse;
}
launch(inst, true, false, serverToJoin, accountToUse);
launch(inst, true, false, targetToJoin, accountToUse);
return;
}
}
@ -1163,6 +1220,12 @@ void Application::performMainStartupAction()
qDebug() << "<> Updater started.";
}
{ // delete instances tmp dirctory
auto instDir = m_settings->get("InstanceDir").toString();
const QString tempRoot = FS::PathCombine(instDir, ".tmp");
FS::deletePath(tempRoot);
}
if (!m_urlsToImport.isEmpty()) {
qDebug() << "<> Importing from url:" << m_urlsToImport;
m_mainWindow->processURLs(m_urlsToImport);
@ -1216,6 +1279,7 @@ void Application::messageReceived(const QByteArray& message)
} else if (command == "launch") {
QString id = received.args["id"];
QString server = received.args["server"];
QString world = received.args["world"];
QString profile = received.args["profile"];
InstancePtr instance;
@ -1230,11 +1294,12 @@ void Application::messageReceived(const QByteArray& message)
return;
}
MinecraftServerTargetPtr serverObject = nullptr;
MinecraftTarget::Ptr serverObject = nullptr;
if (!server.isEmpty()) {
serverObject = std::make_shared<MinecraftServerTarget>(MinecraftServerTarget::parse(server));
serverObject = std::make_shared<MinecraftTarget>(MinecraftTarget::parse(server, false));
} else if (!world.isEmpty()) {
serverObject = std::make_shared<MinecraftTarget>(MinecraftTarget::parse(world, true));
}
MinecraftAccountPtr accountObject;
if (!profile.isEmpty()) {
accountObject = accounts()->getAccountByProfileName(profile);
@ -1283,11 +1348,7 @@ bool Application::openJsonEditor(const QString& filename)
}
}
bool Application::launch(InstancePtr instance,
bool online,
bool demo,
MinecraftServerTargetPtr serverToJoin,
MinecraftAccountPtr accountToUse)
bool Application::launch(InstancePtr instance, bool online, bool demo, MinecraftTarget::Ptr targetToJoin, MinecraftAccountPtr accountToUse)
{
if (m_updateRunning) {
qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed.";
@ -1305,7 +1366,7 @@ bool Application::launch(InstancePtr instance,
controller->setOnline(online);
controller->setDemo(demo);
controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get());
controller->setServerToJoin(serverToJoin);
controller->setTargetToJoin(targetToJoin);
controller->setAccountToUse(accountToUse);
if (window) {
controller->setParentWidget(window);
@ -1476,6 +1537,17 @@ InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString pa
auto& window = extras.window;
if (window) {
// If the window is minimized on macOS or Windows, activate and bring it up
#ifdef Q_OS_MACOS
if (window->isMinimized()) {
window->setWindowState(window->windowState() & ~Qt::WindowMinimized);
}
#elif defined(Q_OS_WIN)
if (window->isMinimized()) {
window->showNormal();
}
#endif
window->raise();
window->activateWindow();
} else {
@ -1483,6 +1555,7 @@ InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString pa
m_openWindows++;
connect(window, &InstanceWindow::isClosing, this, &Application::on_windowClose);
}
if (!page.isEmpty()) {
window->selectPage(page);
}
@ -1611,8 +1684,7 @@ QString Application::getJarPath(QString jarFile)
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME),
#endif
FS::PathCombine(m_rootPath, "jars"),
FS::PathCombine(applicationDirPath(), "jars"),
FS::PathCombine(m_rootPath, "jars"), FS::PathCombine(applicationDirPath(), "jars"),
FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir, for debuging
};
for (QString p : potentialPaths) {

View File

@ -47,8 +47,7 @@
#include <BaseInstance.h>
#include "minecraft/launch/MinecraftServerTarget.h"
#include "ui/themes/CatPack.h"
#include "minecraft/launch/MinecraftTarget.h"
class LaunchController;
class LocalPeer;
@ -193,6 +192,8 @@ class Application : public QApplication {
void globalSettingsClosed();
int currentCatChanged(int index);
void oauthReplyRecieved(QVariantMap);
#ifdef Q_OS_MACOS
void clickedOnDock();
#endif
@ -201,7 +202,7 @@ class Application : public QApplication {
bool launch(InstancePtr instance,
bool online = true,
bool demo = false,
MinecraftServerTargetPtr serverToJoin = nullptr,
MinecraftTarget::Ptr targetToJoin = nullptr,
MinecraftAccountPtr accountToUse = nullptr);
bool kill(InstancePtr instance);
void closeCurrentWindow();
@ -289,6 +290,7 @@ class Application : public QApplication {
QString m_detectedOpenALPath;
QString m_instanceIdToLaunch;
QString m_serverToJoin;
QString m_worldToJoin;
QString m_profileToUse;
bool m_liveCheck = false;
QList<QUrl> m_urlsToImport;

View File

@ -16,6 +16,7 @@
#include <QFile>
#include "BaseInstaller.h"
#include "FileSystem.h"
#include "minecraft/MinecraftInstance.h"
BaseInstaller::BaseInstaller() {}
@ -42,7 +43,7 @@ bool BaseInstaller::add(MinecraftInstance* to)
bool BaseInstaller::remove(MinecraftInstance* from)
{
return QFile::remove(filename(from->instanceRoot()));
return FS::deletePath(filename(from->instanceRoot()));
}
QString BaseInstaller::filename(const QString& root) const

View File

@ -29,7 +29,7 @@ class BaseVersion;
class BaseInstaller {
public:
BaseInstaller();
virtual ~BaseInstaller(){};
virtual ~BaseInstaller() {};
bool isApplied(MinecraftInstance* on);
virtual bool add(MinecraftInstance* to);

View File

@ -64,6 +64,8 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s
m_settings->registerSetting("lastLaunchTime", 0);
m_settings->registerSetting("totalTimePlayed", 0);
if (m_settings->get("totalTimePlayed").toLongLong() < 0)
m_settings->reset("totalTimePlayed");
m_settings->registerSetting("lastTimePlayed", 0);
m_settings->registerSetting("linkedInstances", "[]");
@ -267,13 +269,18 @@ void BaseInstance::setRunning(bool running)
m_isRunning = running;
if (!m_settings->get("RecordGameTime").toBool()) {
emit runningStatusChanged(running);
emit runningStatusChanged(running);
}
void BaseInstance::setMinecraftRunning(bool running)
{
if (!settings()->get("RecordGameTime").toBool()) {
return;
}
if (running) {
m_timeStarted = QDateTime::currentDateTime();
setLastLaunch(m_timeStarted.toMSecsSinceEpoch());
} else {
QDateTime timeEnded = QDateTime::currentDateTime();
@ -283,8 +290,6 @@ void BaseInstance::setRunning(bool running)
emit propertiesChanged(this);
}
emit runningStatusChanged(running);
}
int64_t BaseInstance::totalTimePlayed() const

View File

@ -56,7 +56,7 @@
#include "net/Mode.h"
#include "RuntimeContext.h"
#include "minecraft/launch/MinecraftServerTarget.h"
#include "minecraft/launch/MinecraftTarget.h"
class QDir;
class Task;
@ -104,6 +104,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this<BaseIns
/// be unique.
virtual QString id() const;
void setMinecraftRunning(bool running);
void setRunning(bool running);
bool isRunning() const;
int64_t totalTimePlayed() const;
@ -183,7 +184,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this<BaseIns
virtual Task::Ptr createUpdateTask(Net::Mode mode) = 0;
/// returns a valid launcher (task container)
virtual shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) = 0;
virtual shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) = 0;
/// returns the current launch task (if any)
shared_qobject_ptr<LaunchTask> getLaunchTask();
@ -255,7 +256,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this<BaseIns
/**
* 'print' a verbose description of the instance into a QStringList
*/
virtual QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) = 0;
virtual QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) = 0;
Status currentStatus() const;

View File

@ -126,7 +126,6 @@ set(NET_SOURCES
net/MetaCacheSink.h
net/Logging.h
net/Logging.cpp
net/NetAction.h
net/NetJob.cpp
net/NetJob.h
net/NetUtils.h
@ -139,7 +138,6 @@ set(NET_SOURCES
net/HeaderProxy.h
net/RawHeaderProxy.h
net/ApiHeaderProxy.h
net/StaticHeaderProxy.h
net/ApiDownload.h
net/ApiDownload.cpp
net/ApiUpload.cpp
@ -164,6 +162,8 @@ set(LAUNCH_SOURCES
launch/steps/Update.h
launch/steps/QuitAfterGameStop.cpp
launch/steps/QuitAfterGameStop.h
launch/steps/PrintServers.cpp
launch/steps/PrintServers.h
launch/LaunchStep.cpp
launch/LaunchStep.h
launch/LaunchTask.cpp
@ -210,28 +210,17 @@ set(MINECRAFT_SOURCES
minecraft/auth/AccountData.h
minecraft/auth/AccountList.cpp
minecraft/auth/AccountList.h
minecraft/auth/AccountTask.cpp
minecraft/auth/AccountTask.h
minecraft/auth/AuthRequest.cpp
minecraft/auth/AuthRequest.h
minecraft/auth/AuthSession.cpp
minecraft/auth/AuthSession.h
minecraft/auth/AuthStep.cpp
minecraft/auth/AuthStep.h
minecraft/auth/MinecraftAccount.cpp
minecraft/auth/MinecraftAccount.h
minecraft/auth/Parsers.cpp
minecraft/auth/Parsers.h
minecraft/auth/flows/AuthFlow.cpp
minecraft/auth/flows/AuthFlow.h
minecraft/auth/flows/MSA.cpp
minecraft/auth/flows/MSA.h
minecraft/auth/flows/Offline.cpp
minecraft/auth/flows/Offline.h
minecraft/auth/AuthFlow.cpp
minecraft/auth/AuthFlow.h
minecraft/auth/steps/OfflineStep.cpp
minecraft/auth/steps/OfflineStep.h
minecraft/auth/steps/EntitlementsStep.cpp
minecraft/auth/steps/EntitlementsStep.h
minecraft/auth/steps/GetSkinStep.cpp
@ -240,6 +229,8 @@ set(MINECRAFT_SOURCES
minecraft/auth/steps/LauncherLoginStep.h
minecraft/auth/steps/MinecraftProfileStep.cpp
minecraft/auth/steps/MinecraftProfileStep.h
minecraft/auth/steps/MSADeviceCodeStep.cpp
minecraft/auth/steps/MSADeviceCodeStep.h
minecraft/auth/steps/MSAStep.cpp
minecraft/auth/steps/MSAStep.h
minecraft/auth/steps/XboxAuthorizationStep.cpp
@ -271,8 +262,8 @@ set(MINECRAFT_SOURCES
minecraft/launch/ExtractNatives.h
minecraft/launch/LauncherPartLaunch.cpp
minecraft/launch/LauncherPartLaunch.h
minecraft/launch/MinecraftServerTarget.cpp
minecraft/launch/MinecraftServerTarget.h
minecraft/launch/MinecraftTarget.cpp
minecraft/launch/MinecraftTarget.h
minecraft/launch/PrintInstanceInfo.cpp
minecraft/launch/PrintInstanceInfo.h
minecraft/launch/ReconstructAssets.cpp
@ -374,13 +365,17 @@ set(MINECRAFT_SOURCES
minecraft/AssetsUtils.h
minecraft/AssetsUtils.cpp
# Minecraft services
minecraft/services/CapeChange.cpp
minecraft/services/CapeChange.h
minecraft/services/SkinUpload.cpp
minecraft/services/SkinUpload.h
minecraft/services/SkinDelete.cpp
minecraft/services/SkinDelete.h
# Minecraft skins
minecraft/skins/CapeChange.cpp
minecraft/skins/CapeChange.h
minecraft/skins/SkinUpload.cpp
minecraft/skins/SkinUpload.h
minecraft/skins/SkinDelete.cpp
minecraft/skins/SkinDelete.h
minecraft/skins/SkinModel.cpp
minecraft/skins/SkinModel.h
minecraft/skins/SkinList.cpp
minecraft/skins/SkinList.h
minecraft/Agent.h)
@ -455,6 +450,8 @@ set(TOOLS_SOURCES
tools/JVisualVM.h
tools/MCEditTool.cpp
tools/MCEditTool.h
tools/GenericProfiler.cpp
tools/GenericProfiler.h
)
set(META_SOURCES
@ -626,7 +623,6 @@ set(PRISMUPDATER_SOURCES
net/HttpMetaCache.h
net/Logging.h
net/Logging.cpp
net/NetAction.h
net/NetRequest.cpp
net/NetRequest.h
net/NetJob.cpp
@ -798,8 +794,6 @@ SET(LAUNCHER_SOURCES
ui/InstanceWindow.cpp
# FIXME: maybe find a better home for this.
SkinUtils.cpp
SkinUtils.h
FileIgnoreProxy.cpp
FileIgnoreProxy.h
FastFileIconProvider.cpp
@ -829,6 +823,8 @@ SET(LAUNCHER_SOURCES
ui/themes/DarkTheme.h
ui/themes/ITheme.cpp
ui/themes/ITheme.h
ui/themes/HintOverrideProxyStyle.cpp
ui/themes/HintOverrideProxyStyle.h
ui/themes/SystemTheme.cpp
ui/themes/SystemTheme.h
ui/themes/IconTheme.cpp
@ -1029,8 +1025,6 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ReviewMessageBox.h
ui/dialogs/VersionSelectDialog.cpp
ui/dialogs/VersionSelectDialog.h
ui/dialogs/SkinUploadDialog.cpp
ui/dialogs/SkinUploadDialog.h
ui/dialogs/ResourceDownloadDialog.cpp
ui/dialogs/ResourceDownloadDialog.h
ui/dialogs/ScrollMessageBox.cpp
@ -1044,7 +1038,12 @@ SET(LAUNCHER_SOURCES
ui/dialogs/InstallLoaderDialog.cpp
ui/dialogs/InstallLoaderDialog.h
ui/dialogs/skins/SkinManageDialog.cpp
ui/dialogs/skins/SkinManageDialog.h
# GUI - widgets
ui/widgets/CheckComboBox.cpp
ui/widgets/CheckComboBox.h
ui/widgets/Common.cpp
ui/widgets/Common.h
ui/widgets/CustomCommands.cpp
@ -1173,7 +1172,6 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/NewComponentDialog.ui
ui/dialogs/NewsDialog.ui
ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui
ui/dialogs/ExportPackDialog.ui
ui/dialogs/ExportToModListDialog.ui
@ -1187,6 +1185,8 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/ScrollMessageBox.ui
ui/dialogs/BlockedModsDialog.ui
ui/dialogs/ChooseProviderDialog.ui
ui/dialogs/skins/SkinManageDialog.ui
)
qt_wrap_ui(PRISM_UPDATE_UI
@ -1246,7 +1246,6 @@ target_link_libraries(Launcher_logic
tomlplusplus::tomlplusplus
qdcss
BuildConfig
Katabasis
Qt${QT_VERSION_MAJOR}::Widgets
ghcFilesystem::ghc_filesystem
)
@ -1264,6 +1263,7 @@ target_link_libraries(Launcher_logic
Qt${QT_VERSION_MAJOR}::Concurrent
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::NetworkAuth
${Launcher_QT_LIBS}
)
target_link_libraries(Launcher_logic
@ -1334,7 +1334,6 @@ if(Launcher_BUILD_UPDATER)
Qt${QT_VERSION_MAJOR}::Network
${Launcher_QT_LIBS}
cmark::cmark
Katabasis
)
add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp)
@ -1499,7 +1498,6 @@ if(INSTALL_BUNDLE STREQUAL "full")
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
PATTERN "*qopensslbackend*" EXCLUDE
PATTERN "*qcertonlybackend*" EXCLUDE
)
install(
@ -1510,10 +1508,78 @@ if(INSTALL_BUNDLE STREQUAL "full")
REGEX "dd\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
PATTERN "*qopensslbackend*" EXCLUDE
PATTERN "*qcertonlybackend*" EXCLUDE
)
endif()
# Wayland support
if(EXISTS "${QT_PLUGINS_DIR}/wayland-graphics-integration-client")
install(
DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-client"
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
)
install(
DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-client"
CONFIGURATIONS Release MinSizeRel
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "dd\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
endif()
if(EXISTS "${QT_PLUGINS_DIR}/wayland-graphics-integration-server")
install(
DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-server"
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
)
install(
DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-server"
CONFIGURATIONS Release MinSizeRel
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "dd\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
endif()
if(EXISTS "${QT_PLUGINS_DIR}/wayland-decoration-client")
install(
DIRECTORY "${QT_PLUGINS_DIR}/wayland-decoration-client"
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
)
install(
DIRECTORY "${QT_PLUGINS_DIR}/wayland-decoration-client"
CONFIGURATIONS Release MinSizeRel
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "dd\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
endif()
if(EXISTS "${QT_PLUGINS_DIR}/wayland-shell-integration")
install(
DIRECTORY "${QT_PLUGINS_DIR}/wayland-shell-integration"
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
)
install(
DIRECTORY "${QT_PLUGINS_DIR}/wayland-shell-integration"
CONFIGURATIONS Release MinSizeRel
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "dd\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
endif()
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake"

View File

@ -18,7 +18,7 @@
class DataMigrationTask : public Task {
Q_OBJECT
public:
explicit DataMigrationTask(QObject* parent, const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathmatcher);
explicit DataMigrationTask(QObject* parent, const QString& sourcePath, const QString& targetPath, IPathMatcher::Ptr pathmatcher);
~DataMigrationTask() override = default;
protected:

View File

@ -37,143 +37,33 @@
#include <QDesktopServices>
#include <QDir>
#include <QProcess>
/**
* This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing.
*/
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
template <typename T>
bool IndirectOpen(T callable, qint64* pid_forked = nullptr)
{
auto pid = fork();
if (pid_forked) {
if (pid > 0)
*pid_forked = pid;
else
*pid_forked = 0;
}
if (pid == -1) {
qWarning() << "IndirectOpen failed to fork: " << errno;
return false;
}
// child - do the stuff
if (pid == 0) {
// unset all this garbage so it doesn't get passed to the child process
qunsetenv("LD_PRELOAD");
qunsetenv("LD_LIBRARY_PATH");
qunsetenv("LD_DEBUG");
qunsetenv("QT_PLUGIN_PATH");
qunsetenv("QT_FONTPATH");
// open the URL
auto status = callable();
// detach from the parent process group.
setsid();
// die. now. do not clean up anything, it would just hang forever.
_exit(status ? 0 : 1);
} else {
// parent - assume it worked.
int status;
while (waitpid(pid, &status, 0)) {
if (WIFEXITED(status)) {
return WEXITSTATUS(status) == 0;
}
if (WIFSIGNALED(status)) {
return false;
}
}
return true;
}
}
#endif
#include "FileSystem.h"
namespace DesktopServices {
bool openDirectory(const QString& path, [[maybe_unused]] bool ensureExists)
bool openPath(const QFileInfo& path, bool ensureFolderPathExists)
{
qDebug() << "Opening directory" << path;
QDir parentPath;
QDir dir(path);
if (ensureExists && !dir.exists()) {
parentPath.mkpath(dir.absolutePath());
qDebug() << "Opening path" << path;
if (ensureFolderPathExists) {
FS::ensureFolderPathExists(path);
}
auto f = [&]() { return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath())); };
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (!isSandbox()) {
return IndirectOpen(f);
} else {
return f();
}
#else
return f();
#endif
return openUrl(QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath()));
}
bool openFile(const QString& path)
bool openPath(const QString& path, bool ensureFolderPathExists)
{
qDebug() << "Opening file" << path;
auto f = [&]() { return QDesktopServices::openUrl(QUrl::fromLocalFile(path)); };
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (!isSandbox()) {
return IndirectOpen(f);
} else {
return f();
}
#else
return f();
#endif
}
bool openFile(const QString& application, const QString& path, const QString& workingDirectory, qint64* pid)
{
qDebug() << "Opening file" << path << "using" << application;
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
// FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
if (!isSandbox()) {
return IndirectOpen([&]() { return QProcess::startDetached(application, QStringList() << path, workingDirectory); }, pid);
} else {
return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid);
}
#else
return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid);
#endif
return openPath(QFileInfo(path), ensureFolderPathExists);
}
bool run(const QString& application, const QStringList& args, const QString& workingDirectory, qint64* pid)
{
qDebug() << "Running" << application << "with args" << args.join(' ');
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (!isSandbox()) {
// FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
return IndirectOpen([&]() { return QProcess::startDetached(application, args, workingDirectory); }, pid);
} else {
return QProcess::startDetached(application, args, workingDirectory, pid);
}
#else
return QProcess::startDetached(application, args, workingDirectory, pid);
#endif
}
bool openUrl(const QUrl& url)
{
qDebug() << "Opening URL" << url.toString();
auto f = [&]() { return QDesktopServices::openUrl(url); };
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (!isSandbox()) {
return IndirectOpen(f);
} else {
return f();
}
#else
return f();
#endif
return QDesktopServices::openUrl(url);
}
bool isFlatpak()
@ -194,9 +84,4 @@ bool isSnap()
#endif
}
bool isSandbox()
{
return isSnap() || isFlatpak();
}
} // namespace DesktopServices

View File

@ -3,31 +3,30 @@
#include <QString>
#include <QUrl>
class QFileInfo;
/**
* This wraps around QDesktopServices and adds workarounds where needed
* Use this instead of QDesktopServices!
*/
namespace DesktopServices {
/**
* Open a file in whatever application is applicable
* Open a path in whatever application is applicable.
* @param ensureFolderPathExists Make sure the path exists
*/
bool openFile(const QString& path);
bool openPath(const QFileInfo& path, bool ensureFolderPathExists = false);
/**
* Open a file in the specified application
* Open a path in whatever application is applicable.
* @param ensureFolderPathExists Make sure the path exists
*/
bool openFile(const QString& application, const QString& path, const QString& workingDirectory = QString(), qint64* pid = 0);
bool openPath(const QString& path, bool ensureFolderPathExists = false);
/**
* Run an application
*/
bool run(const QString& application, const QStringList& args, const QString& workingDirectory = QString(), qint64* pid = 0);
/**
* Open a directory
*/
bool openDirectory(const QString& path, bool ensureExists = false);
/**
* Open the URL, most likely in a browser. Maybe.
*/
@ -42,9 +41,4 @@ bool isFlatpak();
* Determine whether the launcher is running in a Snap environment
*/
bool isSnap();
/**
* Determine whether the launcher is running in a sandboxed (Flatpak or Snap) environment
*/
bool isSandbox();
} // namespace DesktopServices

View File

@ -1,4 +1,37 @@
// Licensed under the Apache-2.0 license. See README.md for details.
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can 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
@ -8,12 +41,12 @@
class Exception : public std::exception {
public:
Exception(const QString& message) : std::exception(), m_message(message) { qCritical() << "Exception:" << message; }
Exception(const Exception& other) : std::exception(), m_message(other.cause()) {}
Exception(const QString& message) : std::exception(), m_message(message.toUtf8()) { qCritical() << "Exception:" << message; }
Exception(const Exception& other) : std::exception(), m_message(other.m_message) {}
virtual ~Exception() noexcept {}
const char* what() const noexcept { return m_message.toLatin1().constData(); }
QString cause() const { return m_message; }
const char* what() const noexcept { return m_message.constData(); }
QString cause() const { return QString::fromUtf8(m_message); }
private:
QString m_message;
QByteArray m_message;
};

View File

@ -272,15 +272,19 @@ bool ensureFilePathExists(QString filenamepath)
return success;
}
bool ensureFolderPathExists(QString foldernamepath)
bool ensureFolderPathExists(const QFileInfo folderPath)
{
QFileInfo a(foldernamepath);
QDir dir;
QString ensuredPath = a.filePath();
QString ensuredPath = folderPath.filePath();
bool success = dir.mkpath(ensuredPath);
return success;
}
bool ensureFolderPathExists(const QString folderPathName)
{
return ensureFolderPathExists(QFileInfo(folderPathName));
}
bool copyFileAttributes(QString src, QString dst)
{
#ifdef Q_OS_WIN32
@ -643,6 +647,19 @@ void ExternalLinkFileProcess::runLinkFile()
qDebug() << "Process exited";
}
bool moveByCopy(const QString& source, const QString& dest)
{
if (!copy(source, dest)()) { // copy
qDebug() << "Copy of" << source << "to" << dest << "failed!";
return false;
}
if (!deletePath(source)) { // remove original
qDebug() << "Deletion of" << source << "failed!";
return false;
};
return true;
}
bool move(const QString& source, const QString& dest)
{
std::error_code err;
@ -650,13 +667,14 @@ bool move(const QString& source, const QString& dest)
ensureFilePathExists(dest);
fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err);
if (err) {
qWarning() << "Failed to move file:" << QString::fromStdString(err.message());
qDebug() << "Source file:" << source;
qDebug() << "Destination file:" << dest;
if (err.value() != 0) {
if (moveByCopy(source, dest))
return true;
qDebug() << "Move of" << source << "to" << dest << "failed!";
qWarning() << "Failed to move file:" << QString::fromStdString(err.message()) << QString::number(err.value());
return false;
}
return err.value() == 0;
return true;
}
bool deletePath(QString path)
@ -797,16 +815,68 @@ QString NormalizePath(QString path)
}
}
QString badFilenameChars = "\"\\/?<>:;*|!+\r\n";
static const QString BAD_WIN_CHARS = "<>:\"|?*\r\n";
static const QString BAD_NTFS_CHARS = "<>:\"|?*";
static const QString BAD_HFS_CHARS = ":";
static const QString BAD_FILENAME_CHARS = BAD_WIN_CHARS + "\\/";
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 (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i)))
string[i] = replaceWith;
return string;
}
QString RemoveInvalidPathChars(QString path, QChar replaceWith)
{
QString invalidChars;
#ifdef Q_OS_WIN
invalidChars = BAD_WIN_CHARS;
#endif
// the null character is ignored in this check as it was not a problem until now
switch (statFS(path).fsType) {
case FilesystemType::FAT: // similar to NTFS
/* fallthrough */
case FilesystemType::NTFS:
/* fallthrough */
case FilesystemType::REFS: // similar to NTFS(should be available only on windows)
invalidChars += BAD_NTFS_CHARS;
break;
// case FilesystemType::EXT:
// case FilesystemType::EXT_2_OLD:
// case FilesystemType::EXT_2_3_4:
// case FilesystemType::XFS:
// case FilesystemType::BTRFS:
// case FilesystemType::NFS:
// case FilesystemType::ZFS:
case FilesystemType::APFS:
/* fallthrough */
case FilesystemType::HFS:
/* fallthrough */
case FilesystemType::HFSPLUS:
/* fallthrough */
case FilesystemType::HFSX:
invalidChars += BAD_HFS_CHARS;
break;
// case FilesystemType::FUSEBLK:
// case FilesystemType::F2FS:
// case FilesystemType::UNKNOWN:
default:
break;
}
if (invalidChars.size() != 0) {
for (int i = 0; i < path.length(); i++) {
if (path.at(i) < ' ' || invalidChars.contains(path.at(i))) {
path[i] = replaceWith;
}
}
}
return string;
return path;
}
QString DirNameFromString(QString string, QString inDir)
@ -1581,4 +1651,70 @@ uintmax_t hardLinkCount(const QString& path)
return count;
}
#ifdef Q_OS_WIN
// returns 8.3 file format from long path
QString shortPathName(const QString& file)
{
auto input = file.toStdWString();
std::wstring output;
long length = GetShortPathNameW(input.c_str(), NULL, 0);
if (length == 0)
return {};
// NOTE: this resizing might seem weird...
// when GetShortPathNameW fails, it returns length including null character
// when it succeeds, it returns length excluding null character
// See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx
output.resize(length);
if (GetShortPathNameW(input.c_str(), (LPWSTR)output.c_str(), length) == 0)
return {};
output.resize(length - 1);
QString ret = QString::fromStdWString(output);
return ret;
}
// if the string survives roundtrip through local 8bit encoding...
bool fitsInLocal8bit(const QString& string)
{
return string == QString::fromLocal8Bit(string.toLocal8Bit());
}
QString getPathNameInLocal8bit(const QString& file)
{
if (!fitsInLocal8bit(file)) {
auto path = shortPathName(file);
if (!path.isEmpty()) {
return path;
}
// in case shortPathName fails just return the path as is
}
return file;
}
#endif
QString getUniqueResourceName(const QString& filePath)
{
auto newFileName = filePath;
if (!newFileName.endsWith(".disabled")) {
return newFileName; // prioritize enabled mods
}
newFileName.chop(9);
if (!QFile::exists(newFileName)) {
return filePath;
}
QFileInfo fileInfo(filePath);
auto baseName = fileInfo.completeBaseName();
auto path = fileInfo.absolutePath();
int counter = 1;
do {
if (counter == 1) {
newFileName = FS::PathCombine(path, baseName + ".duplicate");
} else {
newFileName = FS::PathCombine(path, baseName + ".duplicate" + QString::number(counter));
}
counter++;
} while (QFile::exists(newFileName));
return newFileName;
}
} // namespace FS

View File

@ -72,7 +72,7 @@ void appendSafe(const QString& filename, const QByteArray& data);
void append(const QString& filename, const QByteArray& data);
/**
* read data from a file safely\
* read data from a file safely
*/
QByteArray read(const QString& filename);
@ -91,7 +91,13 @@ bool ensureFilePathExists(QString filenamepath);
* Creates all the folders in a path for the specified path
* last segment of the path is treated as a folder name and is created!
*/
bool ensureFolderPathExists(QString filenamepath);
bool ensureFolderPathExists(const QFileInfo folderPath);
/**
* Creates all the folders in a path for the specified path
* last segment of the path is treated as a folder name and is created!
*/
bool ensureFolderPathExists(const QString folderPathName);
/**
* @brief Copies a directory and it's contents from src to dest
@ -234,6 +240,7 @@ class create_link : public QObject {
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalLinked() { return m_linked; }
int totalToLink() { return static_cast<int>(m_links_to_make.size()); }
void runPrivileged() { runPrivileged(QString()); }
void runPrivileged(const QString& offset);
@ -336,6 +343,8 @@ QString NormalizePath(QString path);
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-');
QString RemoveInvalidPathChars(QString string, QChar replaceWith = '-');
QString DirNameFromString(QString string, QString inDir = ".");
/// Checks if the a given Path contains "!"
@ -370,6 +379,7 @@ enum class FilesystemType {
HFSX,
FUSEBLK,
F2FS,
BCACHEFS,
UNKNOWN
};
@ -398,6 +408,7 @@ static const QMap<FilesystemType, QStringList> s_filesystem_type_names = { { Fil
{ FilesystemType::HFSX, { "HFSX" } },
{ FilesystemType::FUSEBLK, { "FUSEBLK" } },
{ FilesystemType::F2FS, { "F2FS" } },
{ FilesystemType::BCACHEFS, { "BCACHEFS" } },
{ FilesystemType::UNKNOWN, { "UNKNOWN" } } };
/**
@ -450,7 +461,7 @@ QString nearestExistentAncestor(const QString& path);
FilesystemInfo statFS(const QString& path);
static const QList<FilesystemType> s_clone_filesystems = { FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS,
FilesystemType::XFS, FilesystemType::REFS };
FilesystemType::XFS, FilesystemType::REFS, FilesystemType::BCACHEFS };
/**
* @brief if the Filesystem is reflink/clone capable
@ -545,4 +556,10 @@ bool canLink(const QString& src, const QString& dst);
uintmax_t hardLinkCount(const QString& path);
#ifdef Q_OS_WIN
QString getPathNameInLocal8bit(const QString& file);
#endif
QString getUniqueResourceName(const QString& filePath);
} // namespace FS

View File

@ -1,10 +1,12 @@
#include "InstanceCopyTask.h"
#include <QDebug>
#include <QtConcurrentRun>
#include <memory>
#include "FileSystem.h"
#include "NullInstance.h"
#include "pathmatcher/RegexpMatcher.h"
#include "settings/INISettingsObject.h"
#include "tasks/Task.h"
InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs)
{
@ -38,38 +40,50 @@ void InstanceCopyTask::executeTask()
{
setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
auto copySaves = [&]() {
QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft"));
QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
QString staging_mc_dir;
if (mcDir.exists() && !dotMCDir.exists())
staging_mc_dir = mcDir.filePath();
else
staging_mc_dir = dotMCDir.filePath();
FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves"));
savesCopy.followSymlinks(true);
return savesCopy();
};
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this, copySaves] {
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] {
if (m_useClone) {
FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath);
folderClone.matcher(m_matcher.get());
folderClone(true);
setProgress(0, folderClone.totalCloned());
connect(&folderClone, &FS::clone::fileCloned,
[this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); });
return folderClone();
} else if (m_useLinks || m_useHardLinks) {
}
if (m_useLinks || m_useHardLinks) {
std::unique_ptr<FS::copy> savesCopy;
if (m_copySaves) {
QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft"));
QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
QString staging_mc_dir;
if (dotMCDir.exists() && !mcDir.exists())
staging_mc_dir = dotMCDir.filePath();
else
staging_mc_dir = mcDir.filePath();
savesCopy = std::make_unique<FS::copy>(FS::PathCombine(m_origInstance->gameRoot(), "saves"),
FS::PathCombine(staging_mc_dir, "saves"));
savesCopy->followSymlinks(true);
(*savesCopy)(true);
setProgress(0, savesCopy->totalCopied());
connect(savesCopy.get(), &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); });
}
FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath);
int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder
folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get());
folderLink(true);
setProgress(0, m_progressTotal + folderLink.totalToLink());
connect(&folderLink, &FS::create_link::fileLinked,
[this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); });
bool there_were_errors = false;
if (!folderLink()) {
#if defined Q_OS_WIN32
if (!m_useHardLinks) {
setProgress(0, m_progressTotal);
qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks";
qDebug() << "attempting to run with privelage";
@ -94,13 +108,11 @@ void InstanceCopyTask::executeTask()
}
}
if (m_copySaves) {
there_were_errors |= !copySaves();
if (savesCopy) {
there_were_errors |= !(*savesCopy)();
}
return got_priv_results && !there_were_errors;
} else {
qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str();
}
#else
qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str();
@ -108,17 +120,19 @@ void InstanceCopyTask::executeTask()
return false;
}
if (m_copySaves) {
there_were_errors |= !copySaves();
if (savesCopy) {
there_were_errors |= !(*savesCopy)();
}
return !there_were_errors;
} else {
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
folderCopy.followSymlinks(false).matcher(m_matcher.get());
return folderCopy();
}
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
folderCopy.followSymlinks(false).matcher(m_matcher.get());
folderCopy(true);
setProgress(0, folderCopy.totalCopied());
connect(&folderCopy, &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); });
return folderCopy();
});
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &InstanceCopyTask::copyFinished);
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &InstanceCopyTask::copyAborted);
@ -142,9 +156,8 @@ void InstanceCopyTask::copyFinished()
if (!m_keepPlaytime) {
inst->resetTimePlayed();
}
if (m_useLinks)
inst->addLinkedInstanceId(m_origInstance->id());
if (m_useLinks) {
inst->addLinkedInstanceId(m_origInstance->id());
auto allowed_symlinks_file = QFileInfo(FS::PathCombine(inst->gameRoot(), "allowed_symlinks.txt"));
QByteArray allowed_symlinks;
@ -171,3 +184,14 @@ void InstanceCopyTask::copyAborted()
emitFailed(tr("Instance folder copy has been aborted."));
return;
}
bool InstanceCopyTask::abort()
{
if (m_copyFutureWatcher.isRunning()) {
m_copyFutureWatcher.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_copyFutureWatcher` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}

View File

@ -19,6 +19,7 @@ class InstanceCopyTask : public InstanceTask {
protected:
//! Entry point for tasks.
virtual void executeTask() override;
bool abort() override;
void copyFinished();
void copyAborted();

View File

@ -2,8 +2,7 @@
#include <QDebug>
#include <QFile>
InstanceCreationTask::InstanceCreationTask() = default;
#include "FileSystem.h"
void InstanceCreationTask::executeTask()
{
@ -47,7 +46,7 @@ void InstanceCreationTask::executeTask()
if (!QFile::exists(path))
continue;
qDebug() << "Removing" << path;
if (!QFile::remove(path)) {
if (!FS::deletePath(path)) {
qCritical() << "Couldn't remove the old conflicting files.";
emitFailed(tr("Failed to remove old conflicting files."));
return;

View File

@ -6,7 +6,7 @@
class InstanceCreationTask : public InstanceTask {
Q_OBJECT
public:
InstanceCreationTask();
InstanceCreationTask() = default;
virtual ~InstanceCreationTask() = default;
protected:

View File

@ -56,10 +56,11 @@
#include <QtConcurrentRun>
#include <algorithm>
#include <memory>
#include <quazip/quazipdir.h>
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent, QMap<QString, QString>&& extra_info)
InstanceImportTask::InstanceImportTask(const QUrl& sourceUrl, QWidget* parent, QMap<QString, QString>&& extra_info)
: m_sourceUrl(sourceUrl), m_extra_info(extra_info), m_parent(parent)
{}
@ -68,15 +69,8 @@ bool InstanceImportTask::abort()
if (!canAbort())
return false;
if (m_filesNetJob)
m_filesNetJob->abort();
if (m_extractFuture.isRunning()) {
// NOTE: The tasks created by QtConcurrent::run() can't actually get cancelled,
// but we can use this call to check the state when the extraction finishes.
m_extractFuture.cancel();
m_extractFuture.waitForFinished();
}
if (task)
task->abort();
return Task::abort();
}
@ -89,7 +83,6 @@ void InstanceImportTask::executeTask()
processZipPack();
} else {
setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
m_downloadRequired = true;
downloadFromUrl();
}
@ -97,115 +90,132 @@ void InstanceImportTask::executeTask()
void InstanceImportTask::downloadFromUrl()
{
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_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network()));
m_filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry));
m_archivePath = entry->getFullPath();
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted);
m_filesNetJob->start();
auto filesNetJob = makeShared<NetJob>(tr("Modpack download"), APPLICATION->network());
filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry));
connect(filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::processZipPack);
connect(filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::setProgress);
connect(filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::emitFailed);
connect(filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::emitAborted);
task.reset(filesNetJob);
filesNetJob->start();
}
void InstanceImportTask::downloadSucceeded()
QString InstanceImportTask::getRootFromZip(QuaZip* zip, const QString& root)
{
processZipPack();
m_filesNetJob.reset();
}
if (!isRunning()) {
return {};
}
QuaZipDir rootDir(zip, root);
for (auto&& fileName : rootDir.entryList(QDir::Files)) {
setDetails(fileName);
if (fileName == "instance.cfg") {
qDebug() << "MultiMC:" << true;
m_modpackType = ModpackType::MultiMC;
return root;
}
if (fileName == "manifest.json") {
qDebug() << "Flame:" << true;
m_modpackType = ModpackType::Flame;
return root;
}
void InstanceImportTask::downloadFailed(QString reason)
{
emitFailed(reason);
m_filesNetJob.reset();
}
QCoreApplication::processEvents();
}
void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total)
{
setProgress(current, total);
}
// Recurse the search to non-ignored subfolders
for (auto&& fileName : rootDir.entryList(QDir::Dirs)) {
if ("overrides/" == fileName)
continue;
void InstanceImportTask::downloadAborted()
{
emitAborted();
m_filesNetJob.reset();
QString result = getRootFromZip(zip, root + fileName);
if (!result.isEmpty())
return result;
}
return {};
}
void InstanceImportTask::processZipPack()
{
setStatus(tr("Extracting modpack"));
setStatus(tr("Attempting to determine instance type"));
QDir extractDir(m_stagingPath);
qDebug() << "Attempting to create instance from" << m_archivePath;
// open the zip and find relevant files in it
m_packZip.reset(new QuaZip(m_archivePath));
if (!m_packZip->open(QuaZip::mdUnzip)) {
auto packZip = std::make_shared<QuaZip>(m_archivePath);
if (!packZip->open(QuaZip::mdUnzip)) {
emitFailed(tr("Unable to open supplied modpack zip file."));
return;
}
QuaZipDir packZipDir(m_packZip.get());
QuaZipDir packZipDir(packZip.get());
qDebug() << "Attempting to determine instance type";
// https://docs.modrinth.com/docs/modpacks/format_definition/#storage
bool modrinthFound = packZipDir.exists("/modrinth.index.json");
bool technicFound = packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json");
QString root;
// NOTE: Prioritize modpack platforms that aren't searched for recursively.
// Especially Flame has a very common filename for its manifest, which may appear inside overrides for example
if (modrinthFound) {
// https://docs.modrinth.com/docs/modpacks/format_definition/#storage
if (packZipDir.exists("/modrinth.index.json")) {
// process as Modrinth pack
qDebug() << "Modrinth:" << modrinthFound;
qDebug() << "Modrinth:" << true;
m_modpackType = ModpackType::Modrinth;
} else if (technicFound) {
} else if (packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json")) {
// process as Technic pack
qDebug() << "Technic:" << technicFound;
extractDir.mkpath(".minecraft");
extractDir.cd(".minecraft");
qDebug() << "Technic:" << true;
extractDir.mkpath("minecraft");
extractDir.cd("minecraft");
m_modpackType = ModpackType::Technic;
} else {
QStringList paths_to_ignore{ "overrides/" };
if (QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg", paths_to_ignore); !mmcRoot.isNull()) {
// process as MultiMC instance/pack
qDebug() << "MultiMC:" << mmcRoot;
root = mmcRoot;
m_modpackType = ModpackType::MultiMC;
} else if (QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json", paths_to_ignore);
!flameRoot.isNull()) {
// process as Flame pack
qDebug() << "Flame:" << flameRoot;
root = flameRoot;
m_modpackType = ModpackType::Flame;
}
root = getRootFromZip(packZip.get());
setDetails("");
}
if (m_modpackType == ModpackType::Unknown) {
emitFailed(tr("Archive does not contain a recognized modpack type."));
return;
}
setStatus(tr("Extracting modpack"));
// make sure we extract just the pack
m_extractFuture =
QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), root, extractDir.absolutePath());
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &InstanceImportTask::extractFinished);
m_extractFutureWatcher.setFuture(m_extractFuture);
auto zipTask = makeShared<MMCZip::ExtractZipTask>(packZip, extractDir, root);
auto progressStep = std::make_shared<TaskStepProgress>();
connect(zipTask.get(), &Task::finished, this, [this, progressStep] {
progressStep->state = TaskStepState::Succeeded;
stepProgress(*progressStep);
});
connect(zipTask.get(), &Task::succeeded, this, &InstanceImportTask::extractFinished);
connect(zipTask.get(), &Task::aborted, this, &InstanceImportTask::emitAborted);
connect(zipTask.get(), &Task::failed, this, [this, progressStep](QString reason) {
progressStep->state = TaskStepState::Failed;
stepProgress(*progressStep);
emitFailed(reason);
});
connect(zipTask.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(zipTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) {
progressStep->update(current, total);
stepProgress(*progressStep);
});
connect(zipTask.get(), &Task::status, this, [this, progressStep](QString status) {
progressStep->status = status;
stepProgress(*progressStep);
});
task.reset(zipTask);
zipTask->start();
}
void InstanceImportTask::extractFinished()
{
m_packZip.reset();
if (m_extractFuture.isCanceled())
return;
if (!m_extractFuture.result().has_value()) {
emitFailed(tr("Failed to extract modpack"));
return;
}
QDir extractDir(m_stagingPath);
qDebug() << "Fixing permissions for extracted pack files...";
@ -324,13 +334,15 @@ void InstanceImportTask::processMultiMC()
m_instIcon = instance.iconKey();
auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon);
if (importIconPath.isNull() || !QFile::exists(importIconPath))
importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), "icon.png");
if (!importIconPath.isNull() && QFile::exists(importIconPath)) {
// import icon
auto iconList = APPLICATION->icons();
if (iconList->iconFileExists(m_instIcon)) {
iconList->deleteIcon(m_instIcon);
}
iconList->installIcons({ importIconPath });
iconList->installIcon(importIconPath, m_instIcon);
}
}
emitSucceeded();

View File

@ -39,54 +39,38 @@
#include <QFutureWatcher>
#include <QUrl>
#include "InstanceTask.h"
#include "QObjectPtr.h"
#include "modplatform/flame/PackManifest.h"
#include "net/NetJob.h"
#include "settings/SettingsObject.h"
#include <memory>
#include <optional>
class QuaZip;
namespace Flame {
class FileResolvingTask;
}
class InstanceImportTask : public InstanceTask {
Q_OBJECT
public:
explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr, QMap<QString, QString>&& extra_info = {});
explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap<QString, QString>&& extra_info = {});
bool abort() override;
const QVector<Flame::File>& getBlockedFiles() const { return m_blockedMods; }
protected:
//! Entry point for tasks.
virtual void executeTask() override;
private:
void processZipPack();
void processMultiMC();
void processTechnic();
void processFlame();
void processModrinth();
QString getRootFromZip(QuaZip* zip, const QString& root = "");
private slots:
void downloadSucceeded();
void downloadFailed(QString reason);
void downloadProgressChanged(qint64 current, qint64 total);
void downloadAborted();
void processZipPack();
void extractFinished();
private: /* data */
NetJob::Ptr m_filesNetJob;
shared_qobject_ptr<Flame::FileResolvingTask> m_modIdResolver;
QUrl m_sourceUrl;
QString m_archivePath;
bool m_downloadRequired = false;
std::unique_ptr<QuaZip> m_packZip;
QFuture<std::optional<QStringList>> m_extractFuture;
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
QVector<Flame::File> m_blockedMods;
Task::Ptr task;
enum class ModpackType {
Unknown,
MultiMC,

View File

@ -38,6 +38,7 @@
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <QJsonArray>
#include <QJsonDocument>
@ -371,13 +372,13 @@ void InstanceList::undoTrashInstance()
auto top = m_trashHistory.pop();
while (QDir(top.polyPath).exists()) {
while (QDir(top.path).exists()) {
top.id += "1";
top.polyPath += "1";
top.path += "1";
}
qDebug() << "Moving" << top.trashPath << "back to" << top.polyPath;
QFile(top.trashPath).rename(top.polyPath);
qDebug() << "Moving" << top.trashPath << "back to" << top.path;
QFile(top.trashPath).rename(top.path);
m_instanceGroupIndex[top.id] = top.groupName;
increaseGroupCount(top.groupName);
@ -634,8 +635,8 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id)
QString inst_type = instanceSettings->get("InstanceType").toString();
// NOTE: Some PolyMC versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a OneSix
// instance
// NOTE: Some launcher versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a
// OneSix instance
if (inst_type == "OneSix" || inst_type.isEmpty()) {
inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot));
} else {
@ -709,6 +710,12 @@ void InstanceList::saveGroupList()
groupsArr.insert(name, groupObj);
}
toplevel.insert("groups", groupsArr);
// empty string represents ungrouped "group"
if (m_collapsedGroups.contains("")) {
QJsonObject ungrouped;
ungrouped.insert("hidden", QJsonValue(true));
toplevel.insert("ungrouped", ungrouped);
}
QJsonDocument doc(toplevel);
try {
FS::write(groupFileName, doc.toJson());
@ -804,6 +811,16 @@ void InstanceList::loadGroupList()
increaseGroupCount(groupName);
}
}
bool ungroupedHidden = false;
if (rootObj.value("ungrouped").isObject()) {
QJsonObject ungrouped = rootObj.value("ungrouped").toObject();
ungroupedHidden = ungrouped.value("hidden").toBool(false);
}
if (ungroupedHidden) {
// empty string represents ungrouped "group"
m_collapsedGroups.insert("");
}
m_groupsLoaded = true;
qDebug() << "Group list loaded.";
}
@ -823,6 +840,9 @@ void InstanceList::on_InstFolderChanged([[maybe_unused]] const Setting& setting,
}
m_instDir = newInstDir;
m_groupsLoaded = false;
beginRemoveRows(QModelIndex(), 0, count());
m_instances.erase(m_instances.begin(), m_instances.end());
endRemoveRows();
emit instancesChanged();
}
}
@ -844,14 +864,16 @@ class InstanceStaging : public Task {
const unsigned maxBackoff = 16;
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))
InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObjectPtr settings)
: m_parent(parent), backoff(minBackoff, maxBackoff)
{
m_stagingPath = parent->getStagedInstancePath();
m_child.reset(child);
m_child->setStagingPath(m_stagingPath);
m_child->setParentSettings(std::move(settings));
connect(child, &Task::succeeded, this, &InstanceStaging::childSucceeded);
connect(child, &Task::failed, this, &InstanceStaging::childFailed);
connect(child, &Task::aborted, this, &InstanceStaging::childAborted);
@ -863,7 +885,7 @@ class InstanceStaging : public Task {
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded);
}
virtual ~InstanceStaging(){};
virtual ~InstanceStaging() {}
// FIXME/TODO: add ability to abort during instance commit retries
bool abort() override
@ -878,14 +900,22 @@ class InstanceStaging : public Task {
bool canAbort() const override { return (m_child && m_child->canAbort()); }
protected:
virtual void executeTask() override { m_child->start(); }
virtual void executeTask() override
{
if (m_stagingPath.isNull()) {
emitFailed(tr("Could not create staging folder"));
return;
}
m_child->start();
}
QStringList warnings() const override { return m_child->warnings(); }
private slots:
void childSucceeded()
{
unsigned sleepTime = backoff();
if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, *m_child.get())) {
if (m_parent->commitStagedInstance(m_stagingPath, *m_child.get(), m_child->group(), *m_child.get())) {
emitSucceeded();
return;
}
@ -894,7 +924,7 @@ class InstanceStaging : public Task {
emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something."));
return;
}
qDebug() << "Failed to commit instance" << m_instance_name.name() << "Initiating backoff:" << sleepTime;
qDebug() << "Failed to commit instance" << m_child->name() << "Initiating backoff:" << sleepTime;
m_backoffTimer.start(sleepTime * 500);
}
void childFailed(const QString& reason)
@ -903,7 +933,11 @@ class InstanceStaging : public Task {
emitFailed(reason);
}
void childAborted() { emitAborted(); }
void childAborted()
{
m_parent->destroyStagingPath(m_stagingPath);
emitAborted();
}
private:
InstanceList* m_parent;
@ -915,34 +949,35 @@ class InstanceStaging : public Task {
ExponentialSeries backoff;
QString m_stagingPath;
unique_qobject_ptr<InstanceTask> m_child;
InstanceName m_instance_name;
QString m_groupName;
QTimer m_backoffTimer;
};
Task* InstanceList::wrapInstanceTask(InstanceTask* task)
{
auto stagingPath = getStagedInstancePath();
task->setStagingPath(stagingPath);
task->setParentSettings(m_globalSettings);
return new InstanceStaging(this, task, stagingPath, *task, task->group());
return new InstanceStaging(this, task, m_globalSettings);
}
QString InstanceList::getStagedInstancePath()
{
QString key = QUuid::createUuid().toString(QUuid::WithoutBraces);
QString tempDir = ".LAUNCHER_TEMP/";
QString relPath = FS::PathCombine(tempDir, key);
QDir rootPath(m_instDir);
auto path = FS::PathCombine(m_instDir, relPath);
if (!rootPath.mkpath(relPath)) {
return QString();
}
const QString tempRoot = FS::PathCombine(m_instDir, ".tmp");
QString result;
int tries = 0;
do {
if (++tries > 256)
return {};
const QString key = QUuid::createUuid().toString(QUuid::Id128).left(6);
result = FS::PathCombine(tempRoot, key);
} while (QFileInfo::exists(result));
if (!QDir::current().mkpath(result))
return {};
#ifdef Q_OS_WIN32
auto tempPath = FS::PathCombine(m_instDir, tempDir);
SetFileAttributesA(tempPath.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
SetFileAttributesA(tempRoot.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
#endif
return path;
return result;
}
bool InstanceList::commitStagedInstance(const QString& path,
@ -953,7 +988,6 @@ bool InstanceList::commitStagedInstance(const QString& path,
if (groupName.isEmpty() && !groupName.isNull())
groupName = QString();
QDir dir;
QString instID;
InstancePtr inst;
@ -977,7 +1011,7 @@ bool InstanceList::commitStagedInstance(const QString& path,
return false;
}
} else {
if (!dir.rename(path, destination)) {
if (!FS::move(path, destination)) {
qWarning() << "Failed to move" << path << "to" << destination;
return false;
}

View File

@ -58,7 +58,7 @@ enum class GroupsState { NotLoaded, Steady, Dirty };
struct TrashHistoryItem {
QString id;
QString polyPath;
QString path;
QString trashPath;
QString groupName;
};

View File

@ -22,7 +22,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider {
public:
explicit InstancePageProvider(InstancePtr parent) { inst = parent; }
virtual ~InstancePageProvider(){};
virtual ~InstancePageProvider() = default;
virtual QList<BasePage*> getPages() override
{
QList<BasePage*> values;
@ -39,7 +39,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider {
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 WorldListPage(onesix, onesix->worldList()));
values.append(new ServersPage(onesix));
// values.append(new GameOptionsPage(onesix.get()));
values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots")));

View File

@ -1,7 +1,11 @@
#include "InstanceTask.h"
#include "Application.h"
#include "settings/SettingsObject.h"
#include "ui/dialogs/CustomMessageBox.h"
#include <QPushButton>
InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name)
{
auto dialog =
@ -20,6 +24,9 @@ InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& ol
ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name)
{
if (APPLICATION->settings()->get("SkipModpackUpdatePrompt").toBool())
return ShouldUpdate::SkipUpdating;
auto info = CustomMessageBox::selectable(
parent, QObject::tr("Similar modpack was found!"),
QObject::tr(
@ -27,16 +34,15 @@ ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name)
"separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before "
"updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).")
.arg(original_version_name),
QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort);
info->setButtonText(QMessageBox::Ok, QObject::tr("Update existing instance"));
info->setButtonText(QMessageBox::Abort, QObject::tr("Create new instance"));
info->setButtonText(QMessageBox::Reset, QObject::tr("Cancel"));
QMessageBox::Information, QMessageBox::Cancel);
QAbstractButton* update = info->addButton(QObject::tr("Update existing instance"), QMessageBox::AcceptRole);
QAbstractButton* skip = info->addButton(QObject::tr("Create new instance"), QMessageBox::ResetRole);
info->exec();
if (info->clickedButton() == info->button(QMessageBox::Ok))
if (info->clickedButton() == update)
return ShouldUpdate::Update;
if (info->clickedButton() == info->button(QMessageBox::Abort))
if (info->clickedButton() == skip)
return ShouldUpdate::SkipUpdating;
return ShouldUpdate::Cancel;
}

View File

@ -24,7 +24,7 @@ class TestCheck : public QObject {
TestCheck(QWidget* parent, QString path, QString args, int minMem, int maxMem, int permGen)
: m_parent(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen)
{}
virtual ~TestCheck(){};
virtual ~TestCheck() {};
void run();

View File

@ -36,12 +36,13 @@
#include "LaunchController.h"
#include "Application.h"
#include "launch/steps/PrintServers.h"
#include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AccountList.h"
#include "ui/InstanceWindow.h"
#include "ui/MainWindow.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/EditAccountDialog.h"
#include "ui/dialogs/ProfileSelectDialog.h"
#include "ui/dialogs/ProfileSetupDialog.h"
#include "ui/dialogs/ProgressDialog.h"
@ -57,7 +58,6 @@
#include "BuildConfig.h"
#include "JavaCommon.h"
#include "launch/steps/TextPrint.h"
#include "minecraft/auth/AccountTask.h"
#include "tasks/Task.h"
LaunchController::LaunchController(QObject* parent) : Task(parent) {}
@ -85,7 +85,7 @@ void LaunchController::decideAccount()
// Find an account to use.
auto accounts = APPLICATION->accounts();
if (accounts->count() <= 0) {
if (accounts->count() <= 0 || !accounts->anyAccountIsValid()) {
// Tell the user they need to log in at least one account in order to play.
auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"),
tr("In order to play Minecraft, you must have at least one Microsoft "
@ -129,12 +129,63 @@ void LaunchController::decideAccount()
}
}
bool LaunchController::askPlayDemo()
{
QMessageBox box(m_parentWidget);
box.setWindowTitle(tr("Play demo?"));
box.setText(
tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play "
"the demo?"));
box.setIcon(QMessageBox::Warning);
auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole);
auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole);
box.setDefaultButton(cancelButton);
box.exec();
return box.clickedButton() == demoButton;
}
QString LaunchController::askOfflineName(QString playerName, bool demo, bool& ok)
{
// we ask the user for a player name
QString message = tr("Choose your offline mode player name.");
if (demo) {
message = tr("Choose your demo mode player name.");
}
QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString();
QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName;
QString name = QInputDialog::getText(m_parentWidget, tr("Player name"), message, QLineEdit::Normal, usedname, &ok);
if (!ok)
return {};
if (name.length()) {
usedname = name;
APPLICATION->settings()->set("LastOfflinePlayerName", usedname);
}
return usedname;
}
void LaunchController::login()
{
decideAccount();
// if no account is selected, we bail
if (!m_accountToUse) {
// if no account is selected, ask about demo
if (!m_demo) {
m_demo = askPlayDemo();
}
if (m_demo) {
// we ask the user for a player name
bool ok = false;
auto name = askOfflineName("Player", m_demo, ok);
if (ok) {
m_session = std::make_shared<AuthSession>();
m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(QRegularExpression("[{}-]")));
launchInstance();
return;
}
}
// if no account is selected, we bail
emitFailed(tr("No account selected for launch."));
return;
}
@ -143,6 +194,13 @@ void LaunchController::login()
bool tryagain = true;
unsigned int tries = 0;
if ((m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) ||
m_accountToUse->shouldRefresh()) {
// Force account refresh on the account used to launch the instance updating the AccountState
// only on first try and if it is not meant to be offline
auto accounts = APPLICATION->accounts();
accounts->requestRefresh(m_accountToUse->internalId());
}
while (tryagain) {
if (tries > 0 && tries % 3 == 0) {
auto result =
@ -161,7 +219,7 @@ void LaunchController::login()
m_accountToUse->fillSession(m_session);
// Launch immediately in true offline mode
if (m_accountToUse->isOffline()) {
if (m_accountToUse->accountType() == AccountType::Offline) {
launchInstance();
return;
}
@ -175,24 +233,12 @@ 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"), message, QLineEdit::Normal, usedname, &ok);
auto name = askOfflineName(m_session->player_name, m_session->demo, ok);
if (!ok) {
tryagain = false;
break;
}
if (name.length()) {
usedname = name;
APPLICATION->settings()->set("LastOfflinePlayerName", usedname);
}
m_session->MakeOffline(usedname);
m_session->MakeOffline(name);
// offline flavored game from here :3
}
if (m_accountToUse->ownsMinecraft()) {
@ -212,20 +258,10 @@ void LaunchController::login()
return;
} else {
// play demo ?
QMessageBox box(m_parentWidget);
box.setWindowTitle(tr("Play demo?"));
box.setText(
tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play "
"the demo?"));
box.setIcon(QMessageBox::Warning);
auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole);
auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole);
box.setDefaultButton(cancelButton);
box.exec();
if (box.clickedButton() == demoButton) {
// play demo here
m_session->MakeDemo();
if (!m_session->demo) {
m_session->demo = askPlayDemo();
}
if (m_session->demo) { // play demo here
launchInstance();
} else {
emitFailed(tr("Launch cancelled - account does not own Minecraft."));
@ -249,12 +285,6 @@ void LaunchController::login()
progDialog.execWithTask(task.get());
continue;
}
// FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that
/*
case AccountState::Queued: {
return;
}
*/
case AccountState::Expired: {
auto errorString = tr("The account has expired and needs to be logged into manually again.");
QMessageBox::warning(m_parentWidget, tr("Account refresh failed"), errorString, QMessageBox::StandardButton::Ok,
@ -294,7 +324,7 @@ void LaunchController::launchInstance()
return;
}
m_launcher = m_instance->createLaunchTask(m_session, m_serverToJoin);
m_launcher = m_instance->createLaunchTask(m_session, m_targetToJoin);
if (!m_launcher) {
emitFailed(tr("Couldn't instantiate a launcher."));
return;
@ -316,26 +346,9 @@ void LaunchController::launchInstance()
online_mode = "online";
// Prepend Server Status
QStringList servers = { "authserver.mojang.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" };
QString resolved_servers = "";
QHostInfo host_info;
QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" };
for (QString server : servers) {
host_info = QHostInfo::fromName(server);
resolved_servers = resolved_servers + server + " resolves to:\n [";
if (!host_info.addresses().isEmpty()) {
for (QHostAddress address : host_info.addresses()) {
resolved_servers = resolved_servers + address.toString();
if (!host_info.addresses().endsWith(address)) {
resolved_servers = resolved_servers + ", ";
}
}
} else {
resolved_servers = resolved_servers + "N/A";
}
resolved_servers = resolved_servers + "]\n\n";
}
m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), resolved_servers, MessageLevel::Launcher));
m_launcher->prependStep(makeShared<PrintServers>(m_launcher.get(), servers));
} else {
online_mode = m_demo ? "demo" : "offline";
}

View File

@ -39,7 +39,7 @@
#include <QObject>
#include "minecraft/auth/MinecraftAccount.h"
#include "minecraft/launch/MinecraftServerTarget.h"
#include "minecraft/launch/MinecraftTarget.h"
class InstanceWindow;
class LaunchController : public Task {
@ -48,7 +48,7 @@ class LaunchController : public Task {
void executeTask() override;
LaunchController(QObject* parent = nullptr);
virtual ~LaunchController(){};
virtual ~LaunchController() = default;
void setInstance(InstancePtr instance) { m_instance = instance; }
@ -62,7 +62,7 @@ class LaunchController : public Task {
void setParentWidget(QWidget* widget) { m_parentWidget = widget; }
void setServerToJoin(MinecraftServerTargetPtr serverToJoin) { m_serverToJoin = std::move(serverToJoin); }
void setTargetToJoin(MinecraftTarget::Ptr targetToJoin) { m_targetToJoin = std::move(targetToJoin); }
void setAccountToUse(MinecraftAccountPtr accountToUse) { m_accountToUse = std::move(accountToUse); }
@ -74,6 +74,8 @@ class LaunchController : public Task {
void login();
void launchInstance();
void decideAccount();
bool askPlayDemo();
QString askOfflineName(QString playerName, bool demo, bool& ok);
private slots:
void readyForLaunch();
@ -92,5 +94,5 @@ class LaunchController : public Task {
MinecraftAccountPtr m_accountToUse = nullptr;
AuthSessionPtr m_session;
shared_qobject_ptr<LaunchTask> m_launcher;
MinecraftServerTargetPtr m_serverToJoin;
MinecraftTarget::Ptr m_targetToJoin;
};

View File

@ -42,6 +42,7 @@
#include <QCoreApplication>
#include <QDebug>
#include <QFileInfo>
#include <QUrl>
#if defined(LAUNCHER_APPLICATION)
@ -50,7 +51,7 @@
namespace MMCZip {
// ours
bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const FilterFunction filter)
bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const FilterFunction& filter)
{
QuaZip modZip(from.filePath());
modZip.open(QuaZip::mdUnzip);
@ -119,9 +120,10 @@ bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool follow
bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks)
{
QuaZip zip(fileCompressed);
zip.setUtf8Enabled(true);
QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
if (!zip.open(QuaZip::mdCreate)) {
QFile::remove(fileCompressed);
FS::deletePath(fileCompressed);
return false;
}
@ -129,7 +131,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files,
zip.close();
if (zip.getZipError() != 0) {
QFile::remove(fileCompressed);
FS::deletePath(fileCompressed);
return false;
}
@ -141,8 +143,9 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files,
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods)
{
QuaZip zipOut(targetJarPath);
zipOut.setUtf8Enabled(true);
if (!zipOut.open(QuaZip::mdCreate)) {
QFile::remove(targetJarPath);
FS::deletePath(targetJarPath);
qCritical() << "Failed to open the minecraft.jar for modding";
return false;
}
@ -160,7 +163,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
if (mod->type() == ResourceType::ZIPFILE) {
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) {
zipOut.close();
QFile::remove(targetJarPath);
FS::deletePath(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false;
}
@ -169,7 +172,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
auto filename = mod->fileinfo();
if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) {
zipOut.close();
QFile::remove(targetJarPath);
FS::deletePath(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false;
}
@ -192,7 +195,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
if (!compressDirFiles(&zipOut, parent_dir, files)) {
zipOut.close();
QFile::remove(targetJarPath);
FS::deletePath(targetJarPath);
qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false;
}
@ -200,7 +203,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
} else {
// Make sure we do not continue launching when something is missing or undefined...
zipOut.close();
QFile::remove(targetJarPath);
FS::deletePath(targetJarPath);
qCritical() << "Failed to add unknown mod type" << mod->fileinfo().fileName() << "to the jar.";
return false;
}
@ -208,7 +211,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key) { return !key.contains("META-INF"); })) {
zipOut.close();
QFile::remove(targetJarPath);
FS::deletePath(targetJarPath);
qCritical() << "Failed to insert minecraft.jar contents.";
return false;
}
@ -216,7 +219,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<M
// Recompress the jar
zipOut.close();
if (zipOut.getZipError() != 0) {
QFile::remove(targetJarPath);
FS::deletePath(targetJarPath);
qCritical() << "Failed to finalize minecraft.jar!";
return false;
}
@ -286,10 +289,11 @@ std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, con
do {
QString file_name = zip->getCurrentFileName();
file_name = FS::RemoveInvalidPathChars(file_name);
if (!file_name.startsWith(subdir))
continue;
auto relative_file_name = QDir::fromNativeSeparators(file_name.remove(0, subdir.size()));
auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size()));
auto original_name = relative_file_name;
// Fix subdirs/files ending with a / getting transformed into absolute paths
@ -327,9 +331,31 @@ std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, con
}
extracted.append(target_file_path);
QFile::setPermissions(target_file_path,
QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser);
auto fileInfo = QFileInfo(target_file_path);
if (fileInfo.isFile()) {
auto permissions = fileInfo.permissions();
auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser |
QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther;
auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
auto newPermisions = (permissions & maxPermisions) | minPermisions;
if (newPermisions != permissions) {
if (!QFile::setPermissions(target_file_path, newPermisions)) {
qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path));
}
}
} else if (fileInfo.isDir()) {
// Ensure the folder has the minimal required permissions
QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadGroup |
QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther;
QFile::Permissions currentPermissions = fileInfo.permissions();
if ((currentPermissions & minimalPermissions) != minimalPermissions) {
if (!QFile::setPermissions(target_file_path, minimalPermissions)) {
qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path));
}
}
}
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
} while (zip->goToNextFile());
@ -463,7 +489,7 @@ auto ExportToZipTask::exportZip() -> ZipResult
auto absolute = file.absoluteFilePath();
auto relative = m_dir.relativeFilePath(absolute);
setStatus("Compresing: " + relative);
setStatus("Compressing: " + relative);
setProgress(m_progress + 1, m_progressTotal);
if (m_follow_symlinks) {
if (file.isSymLink())
@ -487,10 +513,10 @@ auto ExportToZipTask::exportZip() -> ZipResult
void ExportToZipTask::finish()
{
if (m_build_zip_future.isCanceled()) {
QFile::remove(m_output_path);
FS::deletePath(m_output_path);
emitAborted();
} else if (auto result = m_build_zip_future.result(); result.has_value()) {
QFile::remove(m_output_path);
FS::deletePath(m_output_path);
emitFailed(result.value());
} else {
emitSucceeded();
@ -507,6 +533,134 @@ bool ExportToZipTask::abort()
}
return false;
}
#endif
void ExtractZipTask::executeTask()
{
m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); });
connect(&m_zip_watcher, &QFutureWatcher<ZipResult>::finished, this, &ExtractZipTask::finish);
m_zip_watcher.setFuture(m_zip_future);
}
auto ExtractZipTask::extractZip() -> ZipResult
{
auto target = m_output_dir.absolutePath();
auto target_top_dir = QUrl::fromLocalFile(target);
QStringList extracted;
qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input->getZipName() << "to" << target;
auto numEntries = m_input->getEntriesCount();
if (numEntries < 0) {
return ZipResult(tr("Failed to enumerate files in archive"));
}
if (numEntries == 0) {
logWarning(tr("Extracting empty archives seems odd..."));
return ZipResult();
}
if (!m_input->goToFirstFile()) {
return ZipResult(tr("Failed to seek to first file in zip"));
}
setStatus("Extracting files...");
setProgress(0, numEntries);
do {
if (m_zip_future.isCanceled())
return ZipResult();
setProgress(m_progress + 1, m_progressTotal);
QString file_name = m_input->getCurrentFileName();
if (!file_name.startsWith(m_subdirectory))
continue;
auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size()));
auto original_name = relative_file_name;
setStatus("Unziping: " + relative_file_name);
// Fix subdirs/files ending with a / getting transformed into absolute paths
if (relative_file_name.startsWith('/'))
relative_file_name = relative_file_name.mid(1);
// Fix weird "folders with a single file get squashed" thing
QString sub_path;
if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) {
sub_path = relative_file_name.section('/', 0, -2) + '/';
FS::ensureFolderPathExists(FS::PathCombine(target, sub_path));
relative_file_name = relative_file_name.split('/').last();
}
QString target_file_path;
if (relative_file_name.isEmpty()) {
target_file_path = target + '/';
} else {
target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name);
if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/'))
target_file_path += '/';
}
if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) {
return ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2")
.arg(relative_file_name, target));
}
if (!JlCompress::extractFile(m_input.get(), "", target_file_path)) {
JlCompress::removeFile(extracted);
return ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path));
}
extracted.append(target_file_path);
auto fileInfo = QFileInfo(target_file_path);
if (fileInfo.isFile()) {
auto permissions = fileInfo.permissions();
auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser |
QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther;
auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
auto newPermisions = (permissions & maxPermisions) | minPermisions;
if (newPermisions != permissions) {
if (!QFile::setPermissions(target_file_path, newPermisions)) {
logWarning(tr("Could not fix permissions for %1").arg(target_file_path));
}
}
} else if (fileInfo.isDir()) {
// Ensure the folder has the minimal required permissions
QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadGroup |
QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther;
QFile::Permissions currentPermissions = fileInfo.permissions();
if ((currentPermissions & minimalPermissions) != minimalPermissions) {
if (!QFile::setPermissions(target_file_path, minimalPermissions)) {
logWarning(tr("Could not fix permissions for %1").arg(target_file_path));
}
}
}
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
} while (m_input->goToNextFile());
return ZipResult();
}
void ExtractZipTask::finish()
{
if (m_zip_future.isCanceled()) {
emitAborted();
} else if (auto result = m_zip_future.result(); result.has_value()) {
emitFailed(result.value());
} else {
emitSucceeded();
}
}
bool ExtractZipTask::abort()
{
if (m_zip_future.isRunning()) {
m_zip_future.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur
// immediately.
return true;
}
return false;
}
#endif
} // namespace MMCZip

View File

@ -60,7 +60,7 @@ using FilterFunction = std::function<bool(const QString&)>;
/**
* Merge two zip files, using a filter function
*/
bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const FilterFunction filter = nullptr);
bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet<QString>& contained, const FilterFunction& filter = nullptr);
/**
* Compress directory, by providing a list of files to compress
@ -154,7 +154,12 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q
#if defined(LAUNCHER_APPLICATION)
class ExportToZipTask : public Task {
public:
ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
ExportToZipTask(QString outputPath,
QDir dir,
QFileInfoList files,
QString destinationPrefix = "",
bool followSymlinks = false,
bool utf8Enabled = false)
: m_output_path(outputPath)
, m_output(outputPath)
, m_dir(dir)
@ -163,9 +168,15 @@ class ExportToZipTask : public Task {
, m_follow_symlinks(followSymlinks)
{
setAbortable(true);
m_output.setUtf8Enabled(utf8Enabled);
};
ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
: ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks){};
ExportToZipTask(QString outputPath,
QString dir,
QFileInfoList files,
QString destinationPrefix = "",
bool followSymlinks = false,
bool utf8Enabled = false)
: ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled) {};
virtual ~ExportToZipTask() = default;
@ -194,5 +205,30 @@ class ExportToZipTask : public Task {
QFuture<ZipResult> m_build_zip_future;
QFutureWatcher<ZipResult> m_build_zip_watcher;
};
class ExtractZipTask : public Task {
public:
ExtractZipTask(std::shared_ptr<QuaZip> input, QDir outputDir, QString subdirectory = "")
: m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory)
{}
virtual ~ExtractZipTask() = default;
using ZipResult = std::optional<QString>;
protected:
virtual void executeTask() override;
bool abort() override;
ZipResult extractZip();
void finish();
private:
std::shared_ptr<QuaZip> m_input;
QDir m_output_dir;
QString m_subdirectory;
QFuture<ZipResult> m_zip_future;
QFutureWatcher<ZipResult> m_zip_watcher;
};
#endif
} // namespace MMCZip

View File

@ -40,8 +40,8 @@ namespace MangoHud {
QString getLibraryString()
{
/*
* Check for vulkan layers in this order:
/**
* Guess MangoHud install location by searching for vulkan layers in this order:
*
* $VK_LAYER_PATH
* $XDG_DATA_DIRS (/usr/local/share/:/usr/share/)
@ -49,8 +49,9 @@ QString getLibraryString()
* /etc
* $XDG_CONFIG_DIRS (/etc/xdg)
* $XDG_CONFIG_HOME (~/.config)
*
* @returns Absolute path of libMangoHud.so if found and empty QString otherwise.
*/
QStringList vkLayerList;
{
QString home = QDir::homePath();
@ -85,7 +86,7 @@ QString getLibraryString()
vkLayerList << FS::PathCombine(xdgConfigHome, "vulkan", "implicit_layer.d");
}
for (QString vkLayer : vkLayerList) {
for (const QString& vkLayer : vkLayerList) {
// prefer to use architecture specific vulkan layers
QString currentArch = QSysInfo::currentCpuArchitecture();
@ -95,8 +96,8 @@ QString getLibraryString()
QStringList manifestNames = { QString("MangoHud.%1.json").arg(currentArch), "MangoHud.json" };
QString filePath = "";
for (QString manifestName : manifestNames) {
QString filePath{};
for (const QString& manifestName : manifestNames) {
QString tryPath = FS::PathCombine(vkLayer, manifestName);
if (QFile::exists(tryPath)) {
filePath = tryPath;
@ -111,10 +112,23 @@ QString getLibraryString()
auto conf = Json::requireDocument(filePath, vkLayer);
auto confObject = Json::requireObject(conf, vkLayer);
auto layer = Json::ensureObject(confObject, "layer");
return Json::ensureString(layer, "library_path");
QString libraryName = Json::ensureString(layer, "library_path");
#ifdef __GLIBC__
// Check whether mangohud is usable on a glibc based system
if (!libraryName.isEmpty()) {
QString libraryPath = findLibrary(libraryName);
if (!libraryPath.isEmpty()) {
return libraryPath;
}
}
#else
// Without glibc return recorded shared library as-is.
return libraryName;
#endif
}
return QString();
return {};
}
QString findLibrary(QString libName)

View File

@ -46,13 +46,13 @@ class NullInstance : public BaseInstance {
{
setVersionBroken(true);
}
virtual ~NullInstance(){};
virtual ~NullInstance() = default;
void saveNow() override {}
void loadSpecificSettings() override { setSpecificSettingsLoaded(true); }
QString getStatusbarDescription() override { return tr("Unknown instance type"); };
QSet<QString> traits() const override { return {}; };
QString instanceConfigFolder() const override { return instanceRoot(); };
shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr, MinecraftServerTargetPtr) override { return nullptr; }
shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr, MinecraftTarget::Ptr) override { return nullptr; }
shared_qobject_ptr<Task> createUpdateTask([[maybe_unused]] Net::Mode mode) override { return nullptr; }
QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); }
QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); }
@ -64,7 +64,7 @@ class NullInstance : public BaseInstance {
bool canEdit() const override { return false; }
bool canLaunch() const override { return false; }
void populateLaunchMenu(QMenu* menu) override {}
QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override
QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override
{
QStringList out;
out << "Null instance - placeholder.";

View File

@ -13,7 +13,7 @@ class RecursiveFileSystemWatcher : public QObject {
QDir rootDir() const { return m_root; }
// WARNING: setting this to true may be bad for performance
void setWatchFiles(const bool watchFiles);
void setWatchFiles(bool watchFiles);
bool watchFiles() const { return m_watchFiles; }
void setMatcher(IPathMatcher::Ptr matcher) { m_matcher = matcher; }

View File

@ -32,7 +32,7 @@ class ResourceDownloadTask : public SequentialTask {
public:
explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion version,
const std::shared_ptr<ResourceFolderModel> packs,
std::shared_ptr<ResourceFolderModel> packs,
bool is_indexed = true,
QString custom_target_folder = {});
const QString& getFilename() const { return m_pack_version.fileName; }

View File

@ -1,52 +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 "SkinUtils.h"
#include "Application.h"
#include "net/HttpMetaCache.h"
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPainter>
namespace SkinUtils {
/*
* Given a username, return a pixmap of the cached skin (if it exists), QPixmap() otherwise
*/
QPixmap getFaceFromCache(QString username, int height, int width)
{
QFile fskin(APPLICATION->metacache()->resolveEntry("skins", username + ".png")->getFullPath());
if (fskin.exists()) {
QPixmap skinTexture(fskin.fileName());
if (!skinTexture.isNull()) {
QPixmap skin = QPixmap(8, 8);
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
skin.fill(QColorConstants::Transparent);
#else
skin.fill(QColor(0, 0, 0, 0));
#endif
QPainter painter(&skin);
painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8));
painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8));
return skin.scaled(height, width, Qt::KeepAspectRatio);
}
}
return QPixmap();
}
} // namespace SkinUtils

View File

@ -1,22 +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 <QPixmap>
namespace SkinUtils {
QPixmap getFaceFromCache(QString id, int height = 64, int width = 64);
}

View File

@ -212,3 +212,25 @@ QPair<QString, QString> StringUtils::splitFirst(const QString& s, const QRegular
right = s.mid(end);
return qMakePair(left, right);
}
static const QRegularExpression ulMatcher("<\\s*/\\s*ul\\s*>");
QString StringUtils::htmlListPatch(QString htmlStr)
{
int pos = htmlStr.indexOf(ulMatcher);
int imgPos;
while (pos != -1) {
pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the </ul> tag. Add one for zeroeth index
imgPos = htmlStr.indexOf("<img ", pos);
if (imgPos == -1)
break; // no image after the tag
auto textBetween = htmlStr.mid(pos, imgPos - pos).trimmed(); // trim all white spaces
if (textBetween.isEmpty())
htmlStr.insert(pos, "<br>");
pos = htmlStr.indexOf(ulMatcher, pos);
}
return htmlStr;
}

View File

@ -85,4 +85,6 @@ QPair<QString, QString> splitFirst(const QString& s, const QString& sep, Qt::Cas
QPair<QString, QString> splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive);
QPair<QString, QString> splitFirst(const QString& s, const QRegularExpression& re);
QString htmlListPatch(QString htmlStr);
} // namespace StringUtils

View File

@ -14,7 +14,7 @@ class VersionProxyModel : public QAbstractProxyModel {
public:
VersionProxyModel(QObject* parent = 0);
virtual ~VersionProxyModel(){};
virtual ~VersionProxyModel() {};
virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override;
virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override;
@ -28,7 +28,7 @@ class VersionProxyModel : public QAbstractProxyModel {
const FilterMap& filters() const;
const QString& search() const;
void setFilter(const BaseVersionList::ModelRoles column, Filter* filter);
void setFilter(BaseVersionList::ModelRoles column, Filter* filter);
void setSearch(const QString& search);
void clearFilters();
QModelIndex getRecommended() const;

View File

@ -322,7 +322,7 @@ const MMCIcon* IconList::icon(const QString& key) const
bool IconList::deleteIcon(const QString& key)
{
return iconFileExists(key) && QFile::remove(icon(key)->getFilePath());
return iconFileExists(key) && FS::deletePath(icon(key)->getFilePath());
}
bool IconList::trashIcon(const QString& key)

View File

@ -52,7 +52,7 @@ class IconList : public QAbstractListModel {
Q_OBJECT
public:
explicit IconList(const QStringList& builtinPaths, QString path, QObject* parent = 0);
virtual ~IconList(){};
virtual ~IconList() {};
QIcon getIcon(const QString& key) const;
int getIconIndex(const QString& key) const;
@ -67,7 +67,7 @@ class IconList : public QAbstractListModel {
virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
bool addThemeIcon(const QString& key);
bool addIcon(const QString& key, const QString& name, const QString& path, const IconType type);
bool addIcon(const QString& key, const QString& name, const QString& path, IconType type);
void saveIcon(const QString& key, const QString& path, const char* format) const;
bool deleteIcon(const QString& key);
bool trashIcon(const QString& key);

View File

@ -52,8 +52,7 @@ QString findBestIconIn(const QString& folder, const QString& iconKey)
while (it.hasNext()) {
it.next();
auto fileInfo = it.fileInfo();
if (fileInfo.completeBaseName() == iconKey && isIconSuffix(fileInfo.suffix()))
if ((fileInfo.completeBaseName() == iconKey || fileInfo.fileName() == iconKey) && isIconSuffix(fileInfo.suffix()))
return fileInfo.absoluteFilePath();
}
return {};

View File

@ -1,5 +1,4 @@
set(CMAKE_MODULE_PATH "@CMAKE_MODULE_PATH@")
file(GLOB_RECURSE QTPLUGINS "${CMAKE_INSTALL_PREFIX}/@PLUGIN_DEST_DIR@/*@CMAKE_SHARED_LIBRARY_SUFFIX@")
function(gp_resolved_file_type_override resolved_file type_var)
if(resolved_file MATCHES "^/(usr/)?lib/libQt")

View File

@ -55,6 +55,9 @@ void JavaChecker::performCheck()
qDebug() << "Java checker library could not be found. Please check your installation.";
return;
}
#ifdef Q_OS_WIN
checkerJar = FS::getPathNameInLocal8bit(checkerJar);
#endif
QStringList args;

View File

@ -26,8 +26,8 @@ using JavaCheckerJobPtr = shared_qobject_ptr<JavaCheckerJob>;
class JavaCheckerJob : public Task {
Q_OBJECT
public:
explicit JavaCheckerJob(QString job_name) : Task(), m_job_name(job_name){};
virtual ~JavaCheckerJob(){};
explicit JavaCheckerJob(QString job_name) : Task(), m_job_name(job_name) {};
virtual ~JavaCheckerJob() {};
bool addJavaCheckerAction(JavaCheckerPtr base)
{

View File

@ -35,7 +35,7 @@ class JavaInstallList : public BaseVersionList {
public:
explicit JavaInstallList(QObject* parent = 0);
Task::Ptr getLoadTask() override;
[[nodiscard]] Task::Ptr getLoadTask() override;
bool isLoaded() override;
const BaseVersion::Ptr at(int i) const override;
int count() const override;

View File

@ -79,11 +79,9 @@ QProcessEnvironment CleanEnviroment()
QStringList stripped = {
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
"LD_LIBRARY_PATH",
"LD_PRELOAD",
"LD_LIBRARY_PATH", "LD_PRELOAD",
#endif
"QT_PLUGIN_PATH",
"QT_FONTPATH"
"QT_PLUGIN_PATH", "QT_FONTPATH"
};
for (auto key : rawenv.keys()) {
auto value = rawenv.value(key);
@ -184,56 +182,58 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString
else if (keyType == KEY_WOW64_32KEY)
archType = "32";
HKEY jreKey;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, keyName.toStdWString().c_str(), 0, KEY_READ | keyType | KEY_ENUMERATE_SUB_KEYS, &jreKey) ==
ERROR_SUCCESS) {
// Read the current type version from the registry.
// This will be used to find any key that contains the JavaHome value.
for (HKEY baseRegistry : { HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE }) {
HKEY jreKey;
if (RegOpenKeyExW(baseRegistry, keyName.toStdWString().c_str(), 0, KEY_READ | keyType | KEY_ENUMERATE_SUB_KEYS, &jreKey) ==
ERROR_SUCCESS) {
// Read the current type version from the registry.
// This will be used to find any key that contains the JavaHome value.
WCHAR subKeyName[255];
DWORD subKeyNameSize, numSubKeys, retCode;
WCHAR subKeyName[255];
DWORD subKeyNameSize, numSubKeys, retCode;
// Get the number of subkeys
RegQueryInfoKeyW(jreKey, NULL, NULL, NULL, &numSubKeys, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
// Get the number of subkeys
RegQueryInfoKeyW(jreKey, NULL, NULL, NULL, &numSubKeys, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
// Iterate until RegEnumKeyEx fails
if (numSubKeys > 0) {
for (DWORD i = 0; i < numSubKeys; i++) {
subKeyNameSize = 255;
retCode = RegEnumKeyExW(jreKey, i, subKeyName, &subKeyNameSize, NULL, NULL, NULL, NULL);
QString newSubkeyName = QString::fromWCharArray(subKeyName);
if (retCode == ERROR_SUCCESS) {
// Now open the registry key for the version that we just got.
QString newKeyName = keyName + "\\" + newSubkeyName + subkeySuffix;
// Iterate until RegEnumKeyEx fails
if (numSubKeys > 0) {
for (DWORD i = 0; i < numSubKeys; i++) {
subKeyNameSize = 255;
retCode = RegEnumKeyExW(jreKey, i, subKeyName, &subKeyNameSize, NULL, NULL, NULL, NULL);
QString newSubkeyName = QString::fromWCharArray(subKeyName);
if (retCode == ERROR_SUCCESS) {
// Now open the registry key for the version that we just got.
QString newKeyName = keyName + "\\" + newSubkeyName + subkeySuffix;
HKEY newKey;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, newKeyName.toStdWString().c_str(), 0, KEY_READ | KEY_WOW64_64KEY, &newKey) ==
ERROR_SUCCESS) {
// Read the JavaHome value to find where Java is installed.
DWORD valueSz = 0;
if (RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, NULL, &valueSz) == ERROR_SUCCESS) {
WCHAR* value = new WCHAR[valueSz];
RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, (BYTE*)value, &valueSz);
HKEY newKey;
if (RegOpenKeyExW(baseRegistry, newKeyName.toStdWString().c_str(), 0, KEY_READ | keyType, &newKey) ==
ERROR_SUCCESS) {
// Read the JavaHome value to find where Java is installed.
DWORD valueSz = 0;
if (RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, NULL, &valueSz) == ERROR_SUCCESS) {
WCHAR* value = new WCHAR[valueSz];
RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, (BYTE*)value, &valueSz);
QString newValue = QString::fromWCharArray(value);
delete[] value;
QString newValue = QString::fromWCharArray(value);
delete[] value;
// Now, we construct the version object and add it to the list.
JavaInstallPtr javaVersion(new JavaInstall());
// Now, we construct the version object and add it to the list.
JavaInstallPtr javaVersion(new JavaInstall());
javaVersion->id = newSubkeyName;
javaVersion->arch = archType;
javaVersion->path = QDir(FS::PathCombine(newValue, "bin")).absoluteFilePath("javaw.exe");
javas.append(javaVersion);
javaVersion->id = newSubkeyName;
javaVersion->arch = archType;
javaVersion->path = QDir(FS::PathCombine(newValue, "bin")).absoluteFilePath("javaw.exe");
javas.append(javaVersion);
}
RegCloseKey(newKey);
}
RegCloseKey(newKey);
}
}
}
}
RegCloseKey(jreKey);
RegCloseKey(jreKey);
}
}
return javas;
@ -283,6 +283,12 @@ QList<QString> JavaUtils::FindJavaPaths()
QList<JavaInstallPtr> ADOPTIUMJDK64s =
this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI");
// IBM Semeru
QList<JavaInstallPtr> SEMERUJRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI");
QList<JavaInstallPtr> SEMERUJRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI");
QList<JavaInstallPtr> SEMERUJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI");
QList<JavaInstallPtr> SEMERUJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI");
// Microsoft
QList<JavaInstallPtr> MICROSOFTJDK64s =
this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI");
@ -300,6 +306,7 @@ QList<QString> JavaUtils::FindJavaPaths()
java_candidates.append(NEWJRE64s);
java_candidates.append(ADOPTOPENJRE64s);
java_candidates.append(ADOPTIUMJRE64s);
java_candidates.append(SEMERUJRE64s);
java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe"));
@ -308,6 +315,7 @@ QList<QString> JavaUtils::FindJavaPaths()
java_candidates.append(ADOPTOPENJDK64s);
java_candidates.append(FOUNDATIONJDK64s);
java_candidates.append(ADOPTIUMJDK64s);
java_candidates.append(SEMERUJDK64s);
java_candidates.append(MICROSOFTJDK64s);
java_candidates.append(ZULU64s);
java_candidates.append(LIBERICA64s);
@ -316,6 +324,7 @@ QList<QString> JavaUtils::FindJavaPaths()
java_candidates.append(NEWJRE32s);
java_candidates.append(ADOPTOPENJRE32s);
java_candidates.append(ADOPTIUMJRE32s);
java_candidates.append(SEMERUJRE32s);
java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe"));
java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe"));
@ -324,6 +333,7 @@ QList<QString> JavaUtils::FindJavaPaths()
java_candidates.append(ADOPTOPENJDK32s);
java_candidates.append(FOUNDATIONJDK32s);
java_candidates.append(ADOPTIUMJDK32s);
java_candidates.append(SEMERUJDK32s);
java_candidates.append(ZULU32s);
java_candidates.append(LIBERICA32s);
@ -362,13 +372,31 @@ QList<QString> JavaUtils::FindJavaPaths()
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java");
}
auto home = qEnvironmentVariable("HOME");
// javas downloaded by sdkman
QDir sdkmanDir(FS::PathCombine(home, ".sdkman/candidates/java"));
QStringList sdkmanJavas = sdkmanDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
foreach (const QString& java, sdkmanJavas) {
javas.append(sdkmanDir.absolutePath() + "/" + java + "/bin/java");
}
// java in user library folder (like from intellij downloads)
QDir userLibraryJVMDir(FS::PathCombine(home, "Library/Java/JavaVirtualMachines/"));
QStringList userLibraryJVMJavas = userLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
foreach (const QString& java, userLibraryJVMJavas) {
javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java");
}
javas.append(getMinecraftJavaBundle());
javas = addJavasFromEnv(javas);
javas.removeDuplicates();
return javas;
}
#elif defined(Q_OS_LINUX)
#elif defined(Q_OS_LINUX) || defined(Q_OS_OPENBSD) || defined(Q_OS_FREEBSD)
QList<QString> JavaUtils::FindJavaPaths()
{
QList<QString> javas;
@ -393,26 +421,37 @@ QList<QString> JavaUtils::FindJavaPaths()
scanJavaDir(snap + dirPath);
}
};
#if defined(Q_OS_LINUX)
// oracle RPMs
scanJavaDirs("/usr/java");
// general locations used by distro packaging
scanJavaDirs("/usr/lib/jvm");
scanJavaDirs("/usr/lib64/jvm");
scanJavaDirs("/usr/lib32/jvm");
// Gentoo's locations for openjdk and openjdk-bin respectively
scanJavaDir("/usr/lib64");
scanJavaDir("/usr/lib");
scanJavaDir("/opt");
// javas stored in Prism Launcher's folder
scanJavaDirs("java");
// manually installed JDKs in /opt
scanJavaDirs("/opt/jdk");
scanJavaDirs("/opt/jdks");
scanJavaDirs("/opt/ibm"); // IBM Semeru Certified Edition
// flatpak
scanJavaDirs("/app/jdk");
#elif defined(Q_OS_OPENBSD) || defined(Q_OS_FREEBSD)
// ports install to /usr/local on OpenBSD & FreeBSD
scanJavaDirs("/usr/local");
#endif
auto home = qEnvironmentVariable("HOME");
// javas downloaded by IntelliJ
scanJavaDirs(FS::PathCombine(home, ".jdks"));
// javas downloaded by sdkman
scanJavaDirs(FS::PathCombine(home, ".sdkman/candidates/java"));
// javas downloaded by gradle (toolchains)
scanJavaDirs(FS::PathCombine(home, ".gradle/jdks"));
javas.append(getMinecraftJavaBundle());
javas = addJavasFromEnv(javas);
@ -439,26 +478,25 @@ QString JavaUtils::getJavaCheckPath()
QStringList getMinecraftJavaBundle()
{
QString partialPath;
QString executable = "java";
QStringList processpaths;
#if defined(Q_OS_OSX)
partialPath = FS::PathCombine(QDir::homePath(), "Library/Application Support");
processpaths << FS::PathCombine(QDir::homePath(), FS::PathCombine("Library", "Application Support", "minecraft", "runtime"));
#elif defined(Q_OS_WIN32)
partialPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", "");
executable += "w.exe";
auto appDataPath = QProcessEnvironment::systemEnvironment().value("APPDATA", "");
processpaths << FS::PathCombine(QFileInfo(appDataPath).absoluteFilePath(), ".minecraft", "runtime");
// add the microsoft store version of the launcher to the search. the current path is:
// C:\Users\USERNAME\AppData\Local\Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\runtime
auto localAppDataPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", "");
auto minecraftMSStorePath =
FS::PathCombine(QFileInfo(partialPath).absolutePath(), "Local", "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe");
minecraftMSStorePath = FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime");
processpaths << minecraftMSStorePath;
FS::PathCombine(QFileInfo(localAppDataPath).absoluteFilePath(), "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe");
processpaths << FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime");
#else
partialPath = QDir::homePath();
processpaths << FS::PathCombine(QDir::homePath(), ".minecraft", "runtime");
#endif
auto minecraftDataPath = FS::PathCombine(partialPath, ".minecraft", "runtime");
processpaths << minecraftDataPath;
QStringList javas;
while (!processpaths.isEmpty()) {

View File

@ -25,7 +25,7 @@ class LaunchStep : public Task {
Q_OBJECT
public: /* methods */
explicit LaunchStep(LaunchTask* parent) : Task(nullptr), m_parent(parent) { bind(parent); };
virtual ~LaunchStep(){};
virtual ~LaunchStep() {};
private: /* methods */
void bind(LaunchTask* parent);
@ -37,9 +37,9 @@ class LaunchStep : public Task {
void progressReportingRequest();
public slots:
virtual void proceed(){};
virtual void proceed() {};
// called in the opposite order than the Task launch(), used to clean up or otherwise undo things after the launch ends
virtual void finalize(){};
virtual void finalize() {};
protected: /* data */
LaunchTask* m_parent;

View File

@ -55,7 +55,7 @@ class LaunchTask : public Task {
public: /* methods */
static shared_qobject_ptr<LaunchTask> create(InstancePtr inst);
virtual ~LaunchTask(){};
virtual ~LaunchTask() {};
void appendStep(shared_qobject_ptr<LaunchStep> step);
void prependStep(shared_qobject_ptr<LaunchStep> step);

View File

@ -22,8 +22,8 @@
class CheckJava : public LaunchStep {
Q_OBJECT
public:
explicit CheckJava(LaunchTask* parent) : LaunchStep(parent){};
virtual ~CheckJava(){};
explicit CheckJava(LaunchTask* parent) : LaunchStep(parent) {};
virtual ~CheckJava() {};
virtual void executeTask();
virtual bool canAbort() const { return false; }

View File

@ -30,7 +30,7 @@ void LookupServerAddress::setLookupAddress(const QString& lookupAddress)
m_dnsLookup->setName(QString("_minecraft._tcp.%1").arg(lookupAddress));
}
void LookupServerAddress::setOutputAddressPtr(MinecraftServerTargetPtr output)
void LookupServerAddress::setOutputAddressPtr(MinecraftTarget::Ptr output)
{
m_output = std::move(output);
}

View File

@ -19,20 +19,20 @@
#include <launch/LaunchStep.h>
#include <QDnsLookup>
#include "minecraft/launch/MinecraftServerTarget.h"
#include "minecraft/launch/MinecraftTarget.h"
class LookupServerAddress : public LaunchStep {
Q_OBJECT
public:
explicit LookupServerAddress(LaunchTask* parent);
virtual ~LookupServerAddress(){};
virtual ~LookupServerAddress() = default;
virtual void executeTask();
virtual bool abort();
virtual bool canAbort() const { return true; }
void setLookupAddress(const QString& lookupAddress);
void setOutputAddressPtr(MinecraftServerTargetPtr output);
void setOutputAddressPtr(MinecraftTarget::Ptr output);
private slots:
void on_dnsLookupFinished();
@ -42,5 +42,5 @@ class LookupServerAddress : public LaunchStep {
QDnsLookup* m_dnsLookup;
QString m_lookupAddress;
MinecraftServerTargetPtr m_output;
MinecraftTarget::Ptr m_output;
};

View File

@ -22,7 +22,7 @@ class PostLaunchCommand : public LaunchStep {
Q_OBJECT
public:
explicit PostLaunchCommand(LaunchTask* parent);
virtual ~PostLaunchCommand(){};
virtual ~PostLaunchCommand() {};
virtual void executeTask();
virtual bool abort();

View File

@ -22,7 +22,7 @@ class PreLaunchCommand : public LaunchStep {
Q_OBJECT
public:
explicit PreLaunchCommand(LaunchTask* parent);
virtual ~PreLaunchCommand(){};
virtual ~PreLaunchCommand() {};
virtual void executeTask();
virtual bool abort();

View File

@ -0,0 +1,65 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 Leia uwu <leia@tutamail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "PrintServers.h"
#include "QHostInfo"
PrintServers::PrintServers(LaunchTask* parent, const QStringList& servers) : LaunchStep(parent)
{
m_servers = servers;
}
void PrintServers::executeTask()
{
for (QString server : m_servers) {
QHostInfo::lookupHost(server, this, &PrintServers::resolveServer);
}
}
void PrintServers::resolveServer(const QHostInfo& host_info)
{
QString server = host_info.hostName();
QString addresses = server + " resolves to:\n [";
if (!host_info.addresses().isEmpty()) {
for (QHostAddress address : host_info.addresses()) {
addresses += address.toString();
if (!host_info.addresses().endsWith(address)) {
addresses += ", ";
}
}
} else {
addresses += "N/A";
}
addresses += "]\n\n";
m_server_to_address.insert(server, addresses);
// print server info in order once all servers are resolved
if (m_server_to_address.size() >= m_servers.size()) {
for (QString serv : m_servers) {
emit logLine(m_server_to_address.value(serv), MessageLevel::Launcher);
}
emitSucceeded();
}
}
bool PrintServers::canAbort() const
{
return true;
}

View File

@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 Leia uwu <leia@tutamail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <LoggedProcess.h>
#include <java/JavaChecker.h>
#include <launch/LaunchStep.h>
#include <QHostInfo>
class PrintServers : public LaunchStep {
Q_OBJECT
public:
PrintServers(LaunchTask* parent, const QStringList& servers);
virtual void executeTask();
virtual bool canAbort() const;
private:
void resolveServer(const QHostInfo& host_info);
QMap<QString, QString> m_server_to_address;
QStringList m_servers;
};

View File

@ -23,8 +23,8 @@
class QuitAfterGameStop : public LaunchStep {
Q_OBJECT
public:
explicit QuitAfterGameStop(LaunchTask* parent) : LaunchStep(parent){};
virtual ~QuitAfterGameStop(){};
explicit QuitAfterGameStop(LaunchTask* parent) : LaunchStep(parent) {};
virtual ~QuitAfterGameStop() {};
virtual void executeTask();
virtual bool canAbort() const { return false; }

View File

@ -28,7 +28,7 @@ class TextPrint : public LaunchStep {
public:
explicit TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel::Enum level);
explicit TextPrint(LaunchTask* parent, const QString& line, MessageLevel::Enum level);
virtual ~TextPrint(){};
virtual ~TextPrint() {};
virtual void executeTask();
virtual bool canAbort() const;

View File

@ -25,8 +25,8 @@
class Update : public LaunchStep {
Q_OBJECT
public:
explicit Update(LaunchTask* parent, Net::Mode mode) : LaunchStep(parent), m_mode(mode){};
virtual ~Update(){};
explicit Update(LaunchTask* parent, Net::Mode mode) : LaunchStep(parent), m_mode(mode) {};
virtual ~Update() {};
void executeTask() override;
bool canAbort() const override;

View File

@ -15,27 +15,43 @@
#include "BaseEntity.h"
#include "Exception.h"
#include "FileSystem.h"
#include "Json.h"
#include "modplatform/helpers/HashUtils.h"
#include "net/ApiDownload.h"
#include "net/ChecksumValidator.h"
#include "net/HttpMetaCache.h"
#include "net/Mode.h"
#include "net/NetJob.h"
#include "Application.h"
#include "BuildConfig.h"
#include "tasks/Task.h"
namespace Meta {
class ParsingValidator : public Net::Validator {
public: /* con/des */
ParsingValidator(Meta::BaseEntity* entity) : m_entity(entity){};
virtual ~ParsingValidator(){};
ParsingValidator(BaseEntity* entity) : m_entity(entity) {};
virtual ~ParsingValidator() = default;
public: /* methods */
bool init(QNetworkRequest&) override { return true; }
bool init(QNetworkRequest&) override
{
m_data.clear();
return true;
}
bool write(QByteArray& data) override
{
this->m_data.append(data);
return true;
}
bool abort() override { return true; }
bool abort() override
{
m_data.clear();
return true;
}
bool validate(QNetworkReply&) override
{
auto fname = m_entity->localFilename();
@ -52,93 +68,131 @@ class ParsingValidator : public Net::Validator {
private: /* data */
QByteArray m_data;
Meta::BaseEntity* m_entity;
BaseEntity* m_entity;
};
Meta::BaseEntity::~BaseEntity() {}
QUrl Meta::BaseEntity::url() const
QUrl BaseEntity::url() const
{
auto s = APPLICATION->settings();
QString metaOverride = s->get("MetaURLOverride").toString();
if (metaOverride.isEmpty()) {
return QUrl(BuildConfig.META_URL).resolved(localFilename());
} else {
return QUrl(metaOverride).resolved(localFilename());
}
return QUrl(metaOverride).resolved(localFilename());
}
bool Meta::BaseEntity::loadLocalFile()
Task::Ptr BaseEntity::loadTask(Net::Mode mode)
{
const QString fname = QDir("meta").absoluteFilePath(localFilename());
if (!QFile::exists(fname)) {
return false;
}
// TODO: check if the file has the expected checksum
try {
auto doc = Json::requireDocument(fname, fname);
auto obj = Json::requireObject(doc, fname);
parse(obj);
return true;
} catch (const Exception& e) {
qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause());
// just make sure it's gone and we never consider it again.
QFile::remove(fname);
return false;
if (m_task && m_task->isRunning()) {
return m_task;
}
m_task.reset(new BaseEntityLoadTask(this, mode));
return m_task;
}
void Meta::BaseEntity::load(Net::Mode loadType)
bool BaseEntity::isLoaded() const
{
// load local file if nothing is loaded yet
if (!isLoaded()) {
if (loadLocalFile()) {
m_loadStatus = LoadStatus::Local;
// consider it loaded only if the main hash is either empty and was remote loadded or the hashes match and was loaded
return m_sha256.isEmpty() ? m_load_status == LoadStatus::Remote : m_load_status != LoadStatus::NotLoaded && m_sha256 == m_file_sha256;
}
void BaseEntity::setSha256(QString sha256)
{
m_sha256 = sha256;
}
BaseEntity::LoadStatus BaseEntity::status() const
{
return m_load_status;
}
BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode) : m_entity(parent), m_mode(mode) {}
void BaseEntityLoadTask::executeTask()
{
const QString fname = QDir("meta").absoluteFilePath(m_entity->localFilename());
auto hashMatches = false;
// the file exists on disk try to load it
if (QFile::exists(fname)) {
try {
QByteArray fileData;
// read local file if nothing is loaded yet
if (m_entity->m_load_status == BaseEntity::LoadStatus::NotLoaded || m_entity->m_file_sha256.isEmpty()) {
setStatus(tr("Loading local file"));
fileData = FS::read(fname);
m_entity->m_file_sha256 = Hashing::hash(fileData, Hashing::Algorithm::Sha256);
}
// on online the hash needs to match
hashMatches = m_entity->m_sha256 == m_entity->m_file_sha256;
if (m_mode == Net::Mode::Online && !m_entity->m_sha256.isEmpty() && !hashMatches) {
throw Exception("mismatched checksum");
}
// load local file
if (m_entity->m_load_status == BaseEntity::LoadStatus::NotLoaded) {
auto doc = Json::requireDocument(fileData, fname);
auto obj = Json::requireObject(doc, fname);
m_entity->parse(obj);
m_entity->m_load_status = BaseEntity::LoadStatus::Local;
}
} catch (const Exception& e) {
qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause());
// just make sure it's gone and we never consider it again.
FS::deletePath(fname);
m_entity->m_load_status = BaseEntity::LoadStatus::NotLoaded;
}
}
// if we need remote update, run the update task
if (loadType == Net::Mode::Offline || !shouldStartRemoteUpdate()) {
auto wasLoadedOffline = m_entity->m_load_status != BaseEntity::LoadStatus::NotLoaded && m_mode == Net::Mode::Offline;
// if has is not present allways fetch from remote(e.g. the main index file), else only fetch if hash doesn't match
auto wasLoadedRemote = m_entity->m_sha256.isEmpty() ? m_entity->m_load_status == BaseEntity::LoadStatus::Remote : hashMatches;
if (wasLoadedOffline || wasLoadedRemote) {
emitSucceeded();
return;
}
m_updateTask.reset(new NetJob(QObject::tr("Download of meta file %1").arg(localFilename()), APPLICATION->network()));
auto url = this->url();
auto entry = APPLICATION->metacache()->resolveEntry("meta", localFilename());
m_task.reset(new NetJob(QObject::tr("Download of meta file %1").arg(m_entity->localFilename()), APPLICATION->network()));
auto url = m_entity->url();
auto entry = APPLICATION->metacache()->resolveEntry("meta", m_entity->localFilename());
entry->setStale(true);
auto dl = Net::ApiDownload::makeCached(url, entry);
/*
* The validator parses the file and loads it into the object.
* If that fails, the file is not written to storage.
*/
dl->addValidator(new ParsingValidator(this));
m_updateTask->addNetAction(dl);
m_updateStatus = UpdateStatus::InProgress;
QObject::connect(m_updateTask.get(), &NetJob::succeeded, [&]() {
m_loadStatus = LoadStatus::Remote;
m_updateStatus = UpdateStatus::Succeeded;
m_updateTask.reset();
if (!m_entity->m_sha256.isEmpty())
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha256, m_entity->m_sha256));
dl->addValidator(new ParsingValidator(m_entity));
m_task->addNetAction(dl);
m_task->setAskRetry(false);
connect(m_task.get(), &Task::failed, this, &BaseEntityLoadTask::emitFailed);
connect(m_task.get(), &Task::succeeded, this, &BaseEntityLoadTask::emitSucceeded);
connect(m_task.get(), &Task::succeeded, this, [this]() {
m_entity->m_load_status = BaseEntity::LoadStatus::Remote;
m_entity->m_file_sha256 = m_entity->m_sha256;
});
QObject::connect(m_updateTask.get(), &NetJob::failed, [&]() {
m_updateStatus = UpdateStatus::Failed;
m_updateTask.reset();
});
m_updateTask->start();
connect(m_task.get(), &Task::progress, this, &Task::setProgress);
connect(m_task.get(), &Task::stepProgress, this, &BaseEntityLoadTask::propagateStepProgress);
connect(m_task.get(), &Task::status, this, &Task::setStatus);
connect(m_task.get(), &Task::details, this, &Task::setDetails);
m_task->start();
}
bool Meta::BaseEntity::isLoaded() const
bool BaseEntityLoadTask::canAbort() const
{
return m_loadStatus > LoadStatus::NotLoaded;
return m_task ? m_task->canAbort() : false;
}
bool Meta::BaseEntity::shouldStartRemoteUpdate() const
bool BaseEntityLoadTask::abort()
{
// TODO: version-locks and offline mode?
return m_updateStatus != UpdateStatus::InProgress;
}
Task::Ptr Meta::BaseEntity::getCurrentTask()
{
if (m_updateStatus == UpdateStatus::InProgress) {
return m_updateTask;
if (m_task) {
Task::abort();
return m_task->abort();
}
return nullptr;
return Task::abort();
}
} // namespace Meta

View File

@ -17,38 +17,57 @@
#include <QJsonObject>
#include <QObject>
#include "QObjectPtr.h"
#include "net/Mode.h"
#include "net/NetJob.h"
#include "tasks/Task.h"
namespace Meta {
class BaseEntityLoadTask;
class BaseEntity {
friend BaseEntityLoadTask;
public: /* types */
using Ptr = std::shared_ptr<BaseEntity>;
enum class LoadStatus { NotLoaded, Local, Remote };
enum class UpdateStatus { NotDone, InProgress, Failed, Succeeded };
public:
virtual ~BaseEntity();
virtual void parse(const QJsonObject& obj) = 0;
virtual ~BaseEntity() = default;
virtual QString localFilename() const = 0;
virtual QUrl url() const;
bool isLoaded() const;
bool shouldStartRemoteUpdate() const;
LoadStatus status() const;
void load(Net::Mode loadType);
Task::Ptr getCurrentTask();
/* for parsers */
void setSha256(QString sha256);
protected: /* methods */
bool loadLocalFile();
virtual void parse(const QJsonObject& obj) = 0;
[[nodiscard]] Task::Ptr loadTask(Net::Mode loadType = Net::Mode::Online);
protected:
QString m_sha256; // the expected sha256
QString m_file_sha256; // the file sha256
private:
LoadStatus m_loadStatus = LoadStatus::NotLoaded;
UpdateStatus m_updateStatus = UpdateStatus::NotDone;
NetJob::Ptr m_updateTask;
LoadStatus m_load_status = LoadStatus::NotLoaded;
Task::Ptr m_task;
};
class BaseEntityLoadTask : public Task {
Q_OBJECT
public:
explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode);
~BaseEntityLoadTask() override = default;
virtual void executeTask() override;
virtual bool canAbort() const override;
virtual bool abort() override;
private:
BaseEntity* m_entity;
Net::Mode m_mode;
NetJob::Ptr m_task;
};
} // namespace Meta

View File

@ -16,7 +16,10 @@
#include "Index.h"
#include "JsonFormat.h"
#include "QObjectPtr.h"
#include "VersionList.h"
#include "meta/BaseEntity.h"
#include "tasks/SequentialTask.h"
namespace Meta {
Index::Index(QObject* parent) : QAbstractListModel(parent) {}
@ -51,14 +54,17 @@ QVariant Index::data(const QModelIndex& index, int role) const
}
return QVariant();
}
int Index::rowCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : m_lists.size();
}
int Index::columnCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : 1;
}
QVariant Index::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0) {
@ -79,6 +85,7 @@ VersionList::Ptr Index::get(const QString& uid)
if (!out) {
out = std::make_shared<VersionList>(uid);
m_uids[uid] = out;
m_lists.append(out);
}
return out;
}
@ -96,7 +103,7 @@ void Index::parse(const QJsonObject& obj)
void Index::merge(const std::shared_ptr<Index>& other)
{
const QVector<VersionList::Ptr> lists = std::dynamic_pointer_cast<Index>(other)->m_lists;
const QVector<VersionList::Ptr> lists = other->m_lists;
// initial load, no need to merge
if (m_lists.isEmpty()) {
beginResetModel();
@ -123,7 +130,33 @@ void Index::merge(const std::shared_ptr<Index>& other)
void Index::connectVersionList(const int row, const VersionList::Ptr& list)
{
connect(list.get(), &VersionList::nameChanged, this,
[this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << Qt::DisplayRole); });
connect(list.get(), &VersionList::nameChanged, this, [this, row] { emit dataChanged(index(row), index(row), { Qt::DisplayRole }); });
}
Task::Ptr Index::loadVersion(const QString& uid, const QString& version, Net::Mode mode, bool force)
{
if (mode == Net::Mode::Offline) {
return get(uid, version)->loadTask(mode);
}
auto versionList = get(uid);
auto loadTask = makeShared<SequentialTask>(
this, tr("Load meta for %1:%2", "This is for the task name that loads the meta index.").arg(uid, version));
if (status() != BaseEntity::LoadStatus::Remote || force) {
loadTask->addTask(this->loadTask(mode));
}
loadTask->addTask(versionList->loadTask(mode));
loadTask->addTask(versionList->getVersion(version)->loadTask(mode));
return loadTask;
}
Version::Ptr Index::getLoadedVersion(const QString& uid, const QString& version)
{
QEventLoop ev;
auto task = loadVersion(uid, version);
QObject::connect(task.get(), &Task::finished, &ev, &QEventLoop::quit);
task->start();
ev.exec();
return get(uid, version);
}
} // namespace Meta

View File

@ -16,10 +16,10 @@
#pragma once
#include <QAbstractListModel>
#include <memory>
#include "BaseEntity.h"
#include "meta/VersionList.h"
#include "net/Mode.h"
class Task;
@ -30,6 +30,7 @@ class Index : public QAbstractListModel, public BaseEntity {
public:
explicit Index(QObject* parent = nullptr);
explicit Index(const QVector<VersionList::Ptr>& lists, QObject* parent = nullptr);
virtual ~Index() = default;
enum { UidRole = Qt::UserRole, NameRole, ListPtrRole };
@ -47,14 +48,21 @@ class Index : public QAbstractListModel, public BaseEntity {
QVector<VersionList::Ptr> lists() const { return m_lists; }
Task::Ptr loadVersion(const QString& uid, const QString& version = {}, Net::Mode mode = Net::Mode::Online, bool force = false);
// this blocks until the version is loaded
Version::Ptr getLoadedVersion(const QString& uid, const QString& version);
public: // for usage by parsers only
void merge(const std::shared_ptr<Index>& other);
protected:
void parse(const QJsonObject& obj) override;
private:
QVector<VersionList::Ptr> m_lists;
QHash<QString, VersionList::Ptr> m_uids;
void connectVersionList(const int row, const VersionList::Ptr& list);
void connectVersionList(int row, const VersionList::Ptr& list);
};
} // namespace Meta

View File

@ -41,6 +41,7 @@ static std::shared_ptr<Index> parseIndexInternal(const QJsonObject& obj)
std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) {
VersionList::Ptr list = std::make_shared<VersionList>(requireString(obj, "uid"));
list->setName(ensureString(obj, "name", QString()));
list->setSha256(ensureString(obj, "sha256", QString()));
return list;
});
return std::make_shared<Index>(lists);
@ -58,6 +59,9 @@ static Version::Ptr parseCommonVersion(const QString& uid, const QJsonObject& ob
parseRequires(obj, &reqs, "requires");
parseRequires(obj, &conflicts, "conflicts");
version->setRequires(reqs, conflicts);
if (auto sha256 = ensureString(obj, "sha256", QString()); !sha256.isEmpty()) {
version->setSha256(sha256);
}
return version;
}

View File

@ -16,11 +16,9 @@
#pragma once
#include <QJsonObject>
#include <memory>
#include <set>
#include "Exception.h"
#include "meta/BaseEntity.h"
namespace Meta {
class Index;

View File

@ -18,12 +18,9 @@
#include <QDateTime>
#include "JsonFormat.h"
#include "minecraft/PackProfile.h"
Meta::Version::Version(const QString& uid, const QString& version) : BaseVersion(), m_uid(uid), m_version(version) {}
Meta::Version::~Version() {}
QString Meta::Version::descriptor()
{
return m_version;
@ -71,6 +68,9 @@ void Meta::Version::mergeFromList(const Meta::Version::Ptr& other)
if (m_volatile != other->m_volatile) {
setVolatile(other->m_volatile);
}
if (!other->m_sha256.isEmpty()) {
m_sha256 = other->m_sha256;
}
}
void Meta::Version::merge(const Version::Ptr& other)

View File

@ -38,7 +38,7 @@ class Version : public QObject, public BaseVersion, public BaseEntity {
using Ptr = std::shared_ptr<Version>;
explicit Version(const QString& uid, const QString& version);
virtual ~Version();
virtual ~Version() = default;
QString descriptor() override;
QString name() override;
@ -52,7 +52,7 @@ class Version : public QObject, public BaseVersion, public BaseEntity {
const Meta::RequireSet& requiredSet() const { return m_requires; }
VersionFilePtr data() const { return m_data; }
bool isRecommended() const { return m_recommended; }
bool isLoaded() const { return m_data != nullptr; }
bool isLoaded() const { return m_data != nullptr && BaseEntity::isLoaded(); }
void merge(const Version::Ptr& other);
void mergeFromList(const Version::Ptr& other);
@ -64,7 +64,7 @@ class Version : public QObject, public BaseVersion, public BaseEntity {
public: // for usage by format parsers only
void setType(const QString& type);
void setTime(const qint64 time);
void setTime(qint64 time);
void setRequires(const Meta::RequireSet& reqs, const Meta::RequireSet& conflicts);
void setVolatile(bool volatile_);
void setRecommended(bool recommended);

View File

@ -17,8 +17,13 @@
#include <QDateTime>
#include "Application.h"
#include "Index.h"
#include "JsonFormat.h"
#include "Version.h"
#include "meta/BaseEntity.h"
#include "net/Mode.h"
#include "tasks/SequentialTask.h"
namespace Meta {
VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList(parent), m_uid(uid)
@ -28,8 +33,11 @@ VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList(
Task::Ptr VersionList::getLoadTask()
{
load(Net::Mode::Online);
return getCurrentTask();
auto loadTask =
makeShared<SequentialTask>(this, tr("Load meta for %1", "This is for the task name that loads the meta index.").arg(m_uid));
loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online));
loadTask->addTask(this->loadTask(Net::Mode::Online));
return loadTask;
}
bool VersionList::isLoaded()
@ -131,6 +139,8 @@ Version::Ptr VersionList::getVersion(const QString& version)
if (!out) {
out = std::make_shared<Version>(m_uid, version);
m_lookup[version] = out;
setupAddedVersion(m_versions.size(), out);
m_versions.append(out);
}
return out;
}
@ -191,6 +201,9 @@ void VersionList::mergeFromIndex(const VersionList::Ptr& other)
if (m_name != other->m_name) {
setName(other->m_name);
}
if (!other->m_sha256.isEmpty()) {
m_sha256 = other->m_sha256;
}
}
void VersionList::merge(const VersionList::Ptr& other)
@ -198,23 +211,27 @@ void VersionList::merge(const VersionList::Ptr& other)
if (m_name != other->m_name) {
setName(other->m_name);
}
if (!other->m_sha256.isEmpty()) {
m_sha256 = other->m_sha256;
}
// TODO: do not reset the whole model. maybe?
beginResetModel();
m_versions.clear();
if (other->m_versions.isEmpty()) {
qWarning() << "Empty list loaded ...";
}
for (const Version::Ptr& version : other->m_versions) {
for (auto version : other->m_versions) {
// we already have the version. merge the contents
if (m_lookup.contains(version->version())) {
m_lookup.value(version->version())->mergeFromList(version);
auto existing = m_lookup.value(version->version());
existing->mergeFromList(version);
version = existing;
} else {
m_lookup.insert(version->uid(), version);
m_lookup.insert(version->version(), version);
// connect it.
setupAddedVersion(m_versions.size(), version);
m_versions.append(version);
}
// connect it.
setupAddedVersion(m_versions.size(), version);
m_versions.append(version);
m_recommended = getBetterVersion(m_recommended, version);
}
endResetModel();
@ -222,14 +239,15 @@ void VersionList::merge(const VersionList::Ptr& other)
void VersionList::setupAddedVersion(const int row, const Version::Ptr& version)
{
// FIXME: do not disconnect from everythin, disconnect only the lambdas here
version->disconnect();
disconnect(version.get(), &Version::requiresChanged, this, nullptr);
disconnect(version.get(), &Version::timeChanged, this, nullptr);
disconnect(version.get(), &Version::typeChanged, this, nullptr);
connect(version.get(), &Version::requiresChanged, this,
[this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << RequiresRole); });
connect(version.get(), &Version::timeChanged, this,
[this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << TimeRole << SortRole); });
connect(version.get(), &Version::typeChanged, this,
[this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << TypeRole); });
[this, row]() { emit dataChanged(index(row), index(row), { TimeRole, SortRole }); });
connect(version.get(), &Version::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TypeRole }); });
}
BaseVersion::Ptr VersionList::getRecommended() const
@ -237,4 +255,14 @@ BaseVersion::Ptr VersionList::getRecommended() const
return m_recommended;
}
void VersionList::waitToLoad()
{
if (isLoaded())
return;
QEventLoop ev;
auto task = getLoadTask();
QObject::connect(task.get(), &Task::finished, &ev, &QEventLoop::quit);
task->start();
ev.exec();
}
} // namespace Meta

View File

@ -30,13 +30,14 @@ class VersionList : public BaseVersionList, public BaseEntity {
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
public:
explicit VersionList(const QString& uid, QObject* parent = nullptr);
virtual ~VersionList() = default;
using Ptr = std::shared_ptr<VersionList>;
enum Roles { UidRole = Qt::UserRole + 100, TimeRole, RequiresRole, VersionPtrRole };
Task::Ptr getLoadTask() override;
bool isLoaded() override;
[[nodiscard]] Task::Ptr getLoadTask() override;
const BaseVersion::Ptr at(int i) const override;
int count() const override;
void sortVersions() override;
@ -58,6 +59,9 @@ class VersionList : public BaseVersionList, public BaseEntity {
QVector<Version::Ptr> versions() const { return m_versions; }
// this blocks until the version list is loaded
void waitToLoad();
public: // for usage only by parsers
void setName(const QString& name);
void setVersions(const QVector<Version::Ptr>& versions);
@ -79,7 +83,7 @@ class VersionList : public BaseVersionList, public BaseEntity {
Version::Ptr m_recommended;
void setupAddedVersion(const int row, const Version::Ptr& version);
void setupAddedVersion(int row, const Version::Ptr& version);
};
} // namespace Meta
Q_DECLARE_METATYPE(Meta::VersionList::Ptr)

View File

@ -51,6 +51,7 @@
#include "net/Download.h"
#include "Application.h"
#include "net/NetRequest.h"
namespace {
QSet<QString> collectPathsFromDir(QString dirPath)
@ -276,14 +277,13 @@ bool reconstructAssets(QString assetsId, QString resourcesFolder)
} // namespace AssetsUtils
NetAction::Ptr AssetObject::getDownloadAction()
Net::NetRequest::Ptr AssetObject::getDownloadAction()
{
QFileInfo objectFile(getLocalPath());
if ((!objectFile.isFile()) || (objectFile.size() != size)) {
auto objectDL = Net::ApiDownload::makeFile(getUrl(), objectFile.filePath());
if (hash.size()) {
auto rawHash = QByteArray::fromHex(hash.toLatin1());
objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash));
objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, hash));
}
objectDL->setProgress(objectDL->getProgress(), size);
return objectDL;

View File

@ -17,14 +17,14 @@
#include <QMap>
#include <QString>
#include "net/NetAction.h"
#include "net/NetJob.h"
#include "net/NetRequest.h"
struct AssetObject {
QString getRelPath();
QUrl getUrl();
QString getLocalPath();
NetAction::Ptr getDownloadAction();
Net::NetRequest::Ptr getDownloadAction();
QString hash;
qint64 size;

View File

@ -56,18 +56,6 @@ Component::Component(PackProfile* parent, const QString& uid)
m_uid = uid;
}
Component::Component(PackProfile* parent, std::shared_ptr<Meta::Version> version)
{
assert(parent);
m_parent = parent;
m_metaVersion = version;
m_uid = version->uid();
m_version = m_cachedVersion = version->version();
m_cachedName = version->name();
m_loaded = version->isLoaded();
}
Component::Component(PackProfile* parent, const QString& uid, std::shared_ptr<VersionFile> file)
{
assert(parent);
@ -102,9 +90,6 @@ void Component::applyTo(LaunchProfile* profile)
std::shared_ptr<class VersionFile> Component::getVersionFile() const
{
if (m_metaVersion) {
if (!m_metaVersion->isLoaded()) {
m_metaVersion->load(Net::Mode::Online);
}
return m_metaVersion->data();
} else {
return m_file;
@ -131,29 +116,35 @@ int Component::getOrder()
}
return 0;
}
void Component::setOrder(int order)
{
m_orderOverride = true;
m_order = order;
}
QString Component::getID()
{
return m_uid;
}
QString Component::getName()
{
if (!m_cachedName.isEmpty())
return m_cachedName;
return m_uid;
}
QString Component::getVersion()
{
return m_cachedVersion;
}
QString Component::getFilename()
{
return m_parent->patchFilePathForUid(m_uid);
}
QDateTime Component::getReleaseDateTime()
{
if (m_metaVersion) {
@ -198,17 +189,14 @@ bool Component::isCustom()
bool Component::isCustomizable()
{
if (m_metaVersion) {
if (getVersionFile()) {
return true;
}
}
return false;
return m_metaVersion && getVersionFile();
}
bool Component::isRemovable()
{
return !m_important;
}
bool Component::isRevertible()
{
if (isCustom()) {
@ -218,18 +206,18 @@ bool Component::isRevertible()
}
return false;
}
bool Component::isMoveable()
{
// HACK, FIXME: this was too dumb and wouldn't follow dependency constraints anyway. For now hardcoded to 'true'.
return true;
}
bool Component::isVersionChangeable()
{
auto list = getVersionList();
if (list) {
if (!list->isLoaded()) {
list->load(Net::Mode::Online);
}
list->waitToLoad();
return list->count() != 0;
}
return false;
@ -336,7 +324,7 @@ bool Component::revert()
bool result = true;
// just kill the file and reload
if (QFile::exists(filename)) {
result = QFile::remove(filename);
result = FS::deletePath(filename);
}
if (result) {
// file gone...

View File

@ -22,7 +22,6 @@ class Component : public QObject, public ProblemProvider {
Component(PackProfile* parent, const QString& uid);
// DEPRECATED: remove these constructors?
Component(PackProfile* parent, std::shared_ptr<Meta::Version> version);
Component(PackProfile* parent, const QString& uid, std::shared_ptr<VersionFile> file);
virtual ~Component() {}

View File

@ -13,6 +13,7 @@
#include "net/Mode.h"
#include "Application.h"
#include "tasks/Task.h"
/*
* This is responsible for loading the components of a component list AND resolving dependency issues between them
@ -93,9 +94,9 @@ static LoadResult loadComponent(ComponentPtr component, Task::Ptr& loadTask, Net
component->m_loaded = true;
result = LoadResult::LoadedLocal;
} else {
metaVersion->load(netmode);
loadTask = metaVersion->getCurrentTask();
if (loadTask)
loadTask = APPLICATION->metadataIndex()->loadVersion(component->m_uid, component->m_version, netmode);
loadTask->start();
if (netmode == Net::Mode::Online)
result = LoadResult::RequiresRemote;
else if (metaVersion->isLoaded())
result = LoadResult::LoadedLocal;
@ -133,21 +134,6 @@ static LoadResult loadPackProfile(ComponentPtr component, Task::Ptr& loadTask, N
}
*/
static LoadResult loadIndex(Task::Ptr& loadTask, Net::Mode netmode)
{
// FIXME: DECIDE. do we want to run the update task anyway?
if (APPLICATION->metadataIndex()->isLoaded()) {
qDebug() << "Index is already loaded";
return LoadResult::LoadedLocal;
}
APPLICATION->metadataIndex()->load(netmode);
loadTask = APPLICATION->metadataIndex()->getCurrentTask();
if (loadTask) {
return LoadResult::RequiresRemote;
}
// FIXME: this is assuming the load succeeded... did it really?
return LoadResult::LoadedLocal;
}
} // namespace
void ComponentUpdateTask::loadComponents()
@ -156,23 +142,7 @@ void ComponentUpdateTask::loadComponents()
size_t taskIndex = 0;
size_t componentIndex = 0;
d->remoteLoadSuccessful = true;
// load the main index (it is needed to determine if components can revert)
{
// FIXME: tear out as a method? or lambda?
Task::Ptr indexLoadTask;
auto singleResult = loadIndex(indexLoadTask, d->netmode);
result = composeLoadResult(result, singleResult);
if (indexLoadTask) {
qDebug() << "Remote loading is being run for metadata index";
RemoteLoadStatus status;
status.type = RemoteLoadStatus::Type::Index;
d->remoteLoadStatusList.append(status);
connect(indexLoadTask.get(), &Task::succeeded, [=]() { remoteLoadSucceeded(taskIndex); });
connect(indexLoadTask.get(), &Task::failed, [=](const QString& error) { remoteLoadFailed(taskIndex, error); });
connect(indexLoadTask.get(), &Task::aborted, [=]() { remoteLoadFailed(taskIndex, tr("Aborted")); });
taskIndex++;
}
}
// load all the components OR their lists...
for (auto component : d->m_list->d->components) {
Task::Ptr loadTask;
@ -206,12 +176,13 @@ void ComponentUpdateTask::loadComponents()
result = composeLoadResult(result, singleResult);
if (loadTask) {
qDebug() << "Remote loading is being run for" << component->getName();
connect(loadTask.get(), &Task::succeeded, [=]() { remoteLoadSucceeded(taskIndex); });
connect(loadTask.get(), &Task::failed, [=](const QString& error) { remoteLoadFailed(taskIndex, error); });
connect(loadTask.get(), &Task::aborted, [=]() { remoteLoadFailed(taskIndex, tr("Aborted")); });
connect(loadTask.get(), &Task::succeeded, this, [this, taskIndex]() { remoteLoadSucceeded(taskIndex); });
connect(loadTask.get(), &Task::failed, this, [this, taskIndex](const QString& error) { remoteLoadFailed(taskIndex, error); });
connect(loadTask.get(), &Task::aborted, this, [this, taskIndex]() { remoteLoadFailed(taskIndex, tr("Aborted")); });
RemoteLoadStatus status;
status.type = loadType;
status.PackProfileIndex = componentIndex;
status.task = loadTask;
d->remoteLoadStatusList.append(status);
taskIndex++;
}
@ -518,7 +489,14 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly)
void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex)
{
if (static_cast<size_t>(d->remoteLoadStatusList.size()) < taskIndex) {
qWarning() << "Got task index outside of results" << taskIndex;
return;
}
auto& taskSlot = d->remoteLoadStatusList[taskIndex];
disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr);
disconnect(taskSlot.task.get(), &Task::failed, this, nullptr);
disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr);
if (taskSlot.finished) {
qWarning() << "Got multiple results from remote load task" << taskIndex;
return;
@ -538,7 +516,14 @@ void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex)
void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg)
{
if (static_cast<size_t>(d->remoteLoadStatusList.size()) < taskIndex) {
qWarning() << "Got task index outside of results" << taskIndex;
return;
}
auto& taskSlot = d->remoteLoadStatusList[taskIndex];
disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr);
disconnect(taskSlot.task.get(), &Task::failed, this, nullptr);
disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr);
if (taskSlot.finished) {
qWarning() << "Got multiple results from remote load task" << taskIndex;
return;

View File

@ -4,6 +4,7 @@
#include <QString>
#include <cstddef>
#include "net/Mode.h"
#include "tasks/Task.h"
class PackProfile;
@ -13,6 +14,7 @@ struct RemoteLoadStatus {
bool finished = false;
bool succeeded = false;
QString error;
Task::Ptr task;
};
struct ComponentUpdateTaskData {

View File

@ -35,12 +35,27 @@
#include "Library.h"
#include "MinecraftInstance.h"
#include "net/NetRequest.h"
#include <BuildConfig.h>
#include <FileSystem.h>
#include <net/ApiDownload.h>
#include <net/ChecksumValidator.h>
/**
* @brief Collect applicable files for the library.
*
* Depending on whether the library is native or not, it adds paths to the
* appropriate lists for jar files, native libraries for 32-bit, and native
* libraries for 64-bit.
*
* @param runtimeContext The current runtime context.
* @param jar List to store paths for jar files.
* @param native List to store paths for native libraries.
* @param native32 List to store paths for 32-bit native libraries.
* @param native64 List to store paths for 64-bit native libraries.
* @param overridePath Optional path to override the default storage path.
*/
void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
QStringList& jar,
QStringList& native,
@ -49,7 +64,9 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
const QString& overridePath) const
{
bool local = isLocal();
// Lambda function to get the absolute file path
auto actualPath = [&](QString relPath) {
relPath = FS::RemoveInvalidPathChars(relPath);
QFileInfo out(FS::PathCombine(storagePrefix(), relPath));
if (local && !overridePath.isEmpty()) {
QString fileName = out.fileName();
@ -57,6 +74,7 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
}
return out.absoluteFilePath();
};
QString raw_storage = storageSuffix(runtimeContext);
if (isNative()) {
if (raw_storage.contains("${arch}")) {
@ -74,15 +92,29 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
}
}
QList<NetAction::Ptr> Library::getDownloads(const RuntimeContext& runtimeContext,
class HttpMetaCache* cache,
QStringList& failedLocalFiles,
const QString& overridePath) const
/**
* @brief Get download requests for the library files.
*
* Depending on whether the library is native or not, and the current runtime context,
* this function prepares download requests for the necessary files. It handles both local
* and remote files, checks for stale cache entries, and adds checksummed downloads.
*
* @param runtimeContext The current runtime context.
* @param cache Pointer to the HTTP meta cache.
* @param failedLocalFiles List to store paths for failed local files.
* @param overridePath Optional path to override the default storage path.
* @return QList<Net::NetRequest::Ptr> List of download requests.
*/
QList<Net::NetRequest::Ptr> Library::getDownloads(const RuntimeContext& runtimeContext,
class HttpMetaCache* cache,
QStringList& failedLocalFiles,
const QString& overridePath) const
{
QList<NetAction::Ptr> out;
QList<Net::NetRequest::Ptr> out;
bool stale = isAlwaysStale();
bool local = isLocal();
// Lambda function to check if a local file exists
auto check_local_file = [&](QString storage) {
QFileInfo fileinfo(storage);
QString fileName = fileinfo.fileName();
@ -95,6 +127,7 @@ QList<NetAction::Ptr> Library::getDownloads(const RuntimeContext& runtimeContext
return true;
};
// Lambda function to add a download request
auto add_download = [&](QString storage, QString url, QString sha1) {
if (local) {
return check_local_file(storage);
@ -114,9 +147,8 @@ QList<NetAction::Ptr> Library::getDownloads(const RuntimeContext& runtimeContext
options |= Net::Download::Option::MakeEternal;
if (sha1.size()) {
auto rawSha1 = QByteArray::fromHex(sha1.toLatin1());
auto dl = Net::ApiDownload::makeCached(url, entry, options);
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, sha1));
qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url;
out.append(dl);
} else {
@ -195,6 +227,15 @@ QList<NetAction::Ptr> Library::getDownloads(const RuntimeContext& runtimeContext
return out;
}
/**
* @brief Check if the library is active in the given runtime context.
*
* This function evaluates rules to determine if the library should be active,
* considering both general rules and native compatibility.
*
* @param runtimeContext The current runtime context.
* @return bool True if the library is active, false otherwise.
*/
bool Library::isActive(const RuntimeContext& runtimeContext) const
{
bool result = true;
@ -215,16 +256,35 @@ bool Library::isActive(const RuntimeContext& runtimeContext) const
return result;
}
/**
* @brief Check if the library is considered local.
*
* @return bool True if the library is local, false otherwise.
*/
bool Library::isLocal() const
{
return m_hint == "local";
}
/**
* @brief Check if the library is always considered stale.
*
* @return bool True if the library is always stale, false otherwise.
*/
bool Library::isAlwaysStale() const
{
return m_hint == "always-stale";
}
/**
* @brief Get the compatible native classifier for the current runtime context.
*
* This function attempts to match the current runtime context with the appropriate
* native classifier.
*
* @param runtimeContext The current runtime context.
* @return QString The compatible native classifier, or an empty string if none is found.
*/
QString Library::getCompatibleNative(const RuntimeContext& runtimeContext) const
{
// try to match precise classifier "[os]-[arch]"
@ -239,16 +299,31 @@ QString Library::getCompatibleNative(const RuntimeContext& runtimeContext) const
return entry.value();
}
/**
* @brief Set the storage prefix for the library.
*
* @param prefix The storage prefix to set.
*/
void Library::setStoragePrefix(QString prefix)
{
m_storagePrefix = prefix;
}
/**
* @brief Get the default storage prefix for libraries.
*
* @return QString The default storage prefix.
*/
QString Library::defaultStoragePrefix()
{
return "libraries/";
}
/**
* @brief Get the current storage prefix for the library.
*
* @return QString The current storage prefix.
*/
QString Library::storagePrefix() const
{
if (m_storagePrefix.isEmpty()) {
@ -257,6 +332,15 @@ QString Library::storagePrefix() const
return m_storagePrefix;
}
/**
* @brief Get the filename for the library in the current runtime context.
*
* This function determines the appropriate filename for the library, taking into
* account native classifiers if applicable.
*
* @param runtimeContext The current runtime context.
* @return QString The filename of the library.
*/
QString Library::filename(const RuntimeContext& runtimeContext) const
{
if (!m_filename.isEmpty()) {
@ -278,6 +362,15 @@ QString Library::filename(const RuntimeContext& runtimeContext) const
return nativeSpec.getFileName();
}
/**
* @brief Get the display name for the library in the current runtime context.
*
* This function returns the display name for the library, defaulting to the filename
* if no display name is set.
*
* @param runtimeContext The current runtime context.
* @return QString The display name of the library.
*/
QString Library::displayName(const RuntimeContext& runtimeContext) const
{
if (!m_displayname.isEmpty())
@ -285,6 +378,15 @@ QString Library::displayName(const RuntimeContext& runtimeContext) const
return filename(runtimeContext);
}
/**
* @brief Get the storage suffix for the library in the current runtime context.
*
* This function determines the appropriate storage suffix for the library, taking into
* account native classifiers if applicable.
*
* @param runtimeContext The current runtime context.
* @return QString The storage suffix of the library.
*/
QString Library::storageSuffix(const RuntimeContext& runtimeContext) const
{
// non-native? use only the gradle specifier

View File

@ -34,7 +34,6 @@
*/
#pragma once
#include <net/NetAction.h>
#include <QDir>
#include <QList>
#include <QMap>
@ -48,6 +47,7 @@
#include "MojangDownloadInfo.h"
#include "Rule.h"
#include "RuntimeContext.h"
#include "net/NetRequest.h"
class Library;
class MinecraftInstance;
@ -144,10 +144,10 @@ class Library {
bool isForge() const;
// Get a list of downloads for this library
QList<NetAction::Ptr> getDownloads(const RuntimeContext& runtimeContext,
class HttpMetaCache* cache,
QStringList& failedLocalFiles,
const QString& overridePath) const;
QList<Net::NetRequest::Ptr> getDownloads(const RuntimeContext& runtimeContext,
class HttpMetaCache* cache,
QStringList& failedLocalFiles,
const QString& overridePath) const;
QString getCompatibleNative(const RuntimeContext& runtimeContext) const;

View File

@ -173,11 +173,12 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride);
m_settings->registerOverride(global_settings->getSetting("CustomGLFWPath"), nativeLibraryWorkaroundsOverride);
// Peformance related options
// Performance 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);
m_settings->registerOverride(global_settings->getSetting("UseZink"), performanceOverride);
// Miscellaneous
auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false);
@ -195,8 +196,9 @@ void MinecraftInstance::loadSpecificSettings()
}
// Join server on launch, this does not have a global override
m_settings->registerSetting("JoinServerOnLaunch", false);
m_settings->registerSetting({ "JoinServerOnLaunch", "JoinOnLaunch" }, false);
m_settings->registerSetting("JoinServerOnLaunchAddress", "");
m_settings->registerSetting("JoinWorldOnLaunch", "");
// Use account for instance, this does not have a global override
m_settings->registerSetting("UseAccountForInstance", false);
@ -292,10 +294,10 @@ QString MinecraftInstance::gameRoot() const
QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft"));
QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft"));
if (mcDir.exists() && !dotMCDir.exists())
return mcDir.filePath();
else
if (dotMCDir.exists() && !mcDir.exists())
return dotMCDir.filePath();
else
return mcDir.filePath();
}
QString MinecraftInstance::binRoot() const
@ -522,8 +524,7 @@ QStringList MinecraftInstance::javaArguments()
if (javaVersion.isModular() && shouldApplyOnlineFixes())
// allow reflective access to java.net - required by the skin fix
args << "--add-opens"
<< "java.base/java.net=ALL-UNNAMED";
args << "--add-opens" << "java.base/java.net=ALL-UNNAMED";
return args;
}
@ -594,22 +595,23 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
QStringList preloadList;
if (auto value = env.value("LD_PRELOAD"); !value.isEmpty())
preloadList = value.split(QLatin1String(":"));
QStringList libPaths;
if (auto value = env.value("LD_LIBRARY_PATH"); !value.isEmpty())
libPaths = value.split(QLatin1String(":"));
auto mangoHudLibString = MangoHud::getLibraryString();
if (!mangoHudLibString.isEmpty()) {
QFileInfo mangoHudLib(mangoHudLibString);
QString libPath = mangoHudLib.absolutePath();
auto appendLib = [libPath, &preloadList](QString fileName) {
if (QFileInfo(FS::PathCombine(libPath, fileName)).exists())
preloadList << FS::PathCombine(libPath, fileName);
};
// dlsym variant is only needed for OpenGL and not included in the vulkan layer
preloadList << "libMangoHud_dlsym.so"
<< "libMangoHud_opengl.so" << mangoHudLib.fileName();
libPaths << mangoHudLib.absolutePath();
appendLib("libMangoHud_dlsym.so");
appendLib("libMangoHud_opengl.so");
preloadList << mangoHudLibString;
}
env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":")));
env.insert("LD_LIBRARY_PATH", libPaths.join(QLatin1String(":")));
env.insert("MANGOHUD", "1");
}
@ -621,6 +623,13 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only");
env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia");
}
if (settings()->get("UseZink").toBool()) {
// taken from https://wiki.archlinux.org/title/OpenGL#OpenGL_over_Vulkan_(Zink)
env.insert("__GLX_VENDOR_LIBRARY_NAME", "mesa");
env.insert("MESA_LOADER_DRIVER_OVERRIDE", "zink");
env.insert("GALLIUM_DRIVER", "zink");
}
#endif
return env;
}
@ -647,7 +656,7 @@ static QString replaceTokensIn(QString text, QMap<QString, QString> with)
return result;
}
QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) const
QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) const
{
auto profile = m_components->getProfile();
QString args_pattern = profile->getMinecraftArguments();
@ -655,9 +664,17 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine
args_pattern += " --tweakClass " + tweaker;
}
if (serverToJoin && !serverToJoin->address.isEmpty()) {
args_pattern += " --server " + serverToJoin->address;
args_pattern += " --port " + QString::number(serverToJoin->port);
if (targetToJoin) {
if (!targetToJoin->address.isEmpty()) {
if (profile->hasTrait("feature:is_quick_play_multiplayer")) {
args_pattern += " --quickPlayMultiplayer " + targetToJoin->address + ':' + QString::number(targetToJoin->port);
} else {
args_pattern += " --server " + targetToJoin->address;
args_pattern += " --port " + QString::number(targetToJoin->port);
}
} else if (!targetToJoin->world.isEmpty() && profile->hasTrait("feature:is_quick_play_singleplayer")) {
args_pattern += " --quickPlaySingleplayer " + targetToJoin->world;
}
}
QMap<QString, QString> token_mapping;
@ -700,7 +717,7 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine
return parts;
}
QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin)
QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin)
{
QString launchScript;
@ -719,9 +736,13 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS
launchScript += "appletClass " + appletClass + "\n";
}
if (serverToJoin && !serverToJoin->address.isEmpty()) {
launchScript += "serverAddress " + serverToJoin->address + "\n";
launchScript += "serverPort " + QString::number(serverToJoin->port) + "\n";
if (targetToJoin) {
if (!targetToJoin->address.isEmpty()) {
launchScript += "serverAddress " + targetToJoin->address + "\n";
launchScript += "serverPort " + QString::number(targetToJoin->port) + "\n";
} else if (!targetToJoin->world.isEmpty()) {
launchScript += "worldName " + targetToJoin->world + "\n";
}
}
// generic minecraft params
@ -774,16 +795,15 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS
return launchScript;
}
QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin)
QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin)
{
QStringList out;
out << "Main Class:"
<< " " + getMainClass() << "";
out << "Native path:"
<< " " + getNativePath() << "";
out << "Main Class:" << " " + getMainClass() << "";
out << "Native path:" << " " + getNativePath() << "";
auto profile = m_components->getProfile();
// traits
auto alltraits = traits();
if (alltraits.size()) {
out << "Traits:";
@ -793,6 +813,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
out << "";
}
// native libraries
auto settings = this->settings();
bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool();
bool nativeGLFW = settings->get("UseNativeGLFW").toBool();
@ -828,6 +849,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
out << "";
}
// mods and core mods
auto printModList = [&](const QString& label, ModFolderModel& model) {
if (model.size()) {
out << QString("%1:").arg(label);
@ -856,6 +878,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
printModList("Mods", *(loaderModList().get()));
printModList("Core Mods", *(coreModList().get()));
// jar mods
auto& jarMods = profile->getJarMods();
if (jarMods.size()) {
out << "Jar Mods:";
@ -871,11 +894,13 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
out << "";
}
auto params = processMinecraftArgs(nullptr, serverToJoin);
// minecraft arguments
auto params = processMinecraftArgs(nullptr, targetToJoin);
out << "Params:";
out << " " + params.join(' ');
out << "";
// window size
QString windowParams;
if (settings->get("LaunchMaximized").toBool()) {
out << "Window size: max (if available)";
@ -987,7 +1012,7 @@ QString MinecraftInstance::getStatusbarDescription()
QString description;
description.append(tr("Minecraft %1").arg(mcVersion));
if (m_settings->get("ShowGameTime").toBool()) {
if (lastTimePlayed() > 0) {
if (lastTimePlayed() > 0 && lastLaunch() > 0) {
QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch());
description.append(
tr(", last played on %1 for %2")
@ -1021,7 +1046,7 @@ Task::Ptr MinecraftInstance::createUpdateTask(Net::Mode mode)
return nullptr;
}
shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin)
shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin)
{
updateRuntimeContext();
// FIXME: get rid of shared_from_this ...
@ -1045,16 +1070,23 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(makeShared<CreateGameFolders>(pptr));
}
if (!serverToJoin && settings()->get("JoinServerOnLaunch").toBool()) {
if (!targetToJoin && settings()->get("JoinOnLaunch").toBool()) {
QString fullAddress = settings()->get("JoinServerOnLaunchAddress").toString();
serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(fullAddress)));
if (!fullAddress.isEmpty()) {
targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(fullAddress, false)));
} else {
QString world = settings()->get("JoinWorldOnLaunch").toString();
if (!world.isEmpty()) {
targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(world, true)));
}
}
}
if (serverToJoin && serverToJoin->port == 25565) {
if (targetToJoin && targetToJoin->port == 25565) {
// Resolve server address to join on launch
auto step = makeShared<LookupServerAddress>(pptr);
step->setLookupAddress(serverToJoin->address);
step->setOutputAddressPtr(serverToJoin);
step->setLookupAddress(targetToJoin->address);
step->setOutputAddressPtr(targetToJoin);
process->appendStep(step);
}
@ -1087,7 +1119,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
// print some instance info here...
{
process->appendStep(makeShared<PrintInstanceInfo>(pptr, session, serverToJoin));
process->appendStep(makeShared<PrintInstanceInfo>(pptr, session, targetToJoin));
}
// extract native jars if needed
@ -1110,7 +1142,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
auto step = makeShared<LauncherPartLaunch>(pptr);
step->setWorkingDirectory(gameRoot());
step->setAuthSession(session);
step->setServerToJoin(serverToJoin);
step->setTargetToJoin(targetToJoin);
process->appendStep(step);
}

View File

@ -39,7 +39,7 @@
#include <QDir>
#include <QProcess>
#include "BaseInstance.h"
#include "minecraft/launch/MinecraftServerTarget.h"
#include "minecraft/launch/MinecraftTarget.h"
#include "minecraft/mod/Mod.h"
class ModFolderModel;
@ -56,7 +56,7 @@ class MinecraftInstance : public BaseInstance {
Q_OBJECT
public:
MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir);
virtual ~MinecraftInstance(){};
virtual ~MinecraftInstance() = default;
virtual void saveNow() override;
void loadSpecificSettings() override;
@ -121,11 +121,11 @@ class MinecraftInstance : public BaseInstance {
////// Launch stuff //////
Task::Ptr createUpdateTask(Net::Mode mode) override;
shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override;
shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) override;
QStringList extraArguments() override;
QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override;
QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override;
QList<Mod*> getJarMods() const;
QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin);
QString createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin);
/// get arguments passed to java
QStringList javaArguments();
QString getLauncher();
@ -155,7 +155,7 @@ class MinecraftInstance : public BaseInstance {
virtual QString getMainClass() const;
// FIXME: remove
virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) const;
virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) const;
virtual JavaVersion getJavaVersion();

View File

@ -31,7 +31,7 @@ class MinecraftLoadAndCheck : public Task {
Q_OBJECT
public:
explicit MinecraftLoadAndCheck(MinecraftInstance* inst, QObject* parent = 0);
virtual ~MinecraftLoadAndCheck(){};
virtual ~MinecraftLoadAndCheck() {};
void executeTask() override;
private slots:

View File

@ -16,32 +16,22 @@
#include "MinecraftUpdate.h"
#include "MinecraftInstance.h"
#include <QDataStream>
#include <QFile>
#include <QFileInfo>
#include <QTextStream>
#include <FileSystem.h>
#include "BaseInstance.h"
#include "minecraft/Library.h"
#include "minecraft/PackProfile.h"
#include "tasks/SequentialTask.h"
#include "update/AssetUpdateTask.h"
#include "update/FMLLibrariesTask.h"
#include "update/FoldersTask.h"
#include "update/LibrariesTask.h"
#include <meta/Index.h>
#include <meta/Version.h>
MinecraftUpdate::MinecraftUpdate(MinecraftInstance* inst, QObject* parent) : Task(parent), m_inst(inst) {}
MinecraftUpdate::MinecraftUpdate(MinecraftInstance* inst, QObject* parent) : SequentialTask(parent), m_inst(inst) {}
void MinecraftUpdate::executeTask()
{
m_tasks.clear();
m_queue.clear();
// create folders
{
m_tasks.append(makeShared<FoldersTask>(m_inst));
addTask(makeShared<FoldersTask>(m_inst));
}
// add metadata update task if necessary
@ -50,121 +40,24 @@ void MinecraftUpdate::executeTask()
components->reload(Net::Mode::Online);
auto task = components->getCurrentTask();
if (task) {
m_tasks.append(task);
addTask(task);
}
}
// libraries download
{
m_tasks.append(makeShared<LibrariesTask>(m_inst));
addTask(makeShared<LibrariesTask>(m_inst));
}
// FML libraries download and copy into the instance
{
m_tasks.append(makeShared<FMLLibrariesTask>(m_inst));
addTask(makeShared<FMLLibrariesTask>(m_inst));
}
// assets update
{
m_tasks.append(makeShared<AssetUpdateTask>(m_inst));
addTask(makeShared<AssetUpdateTask>(m_inst));
}
if (!m_preFailure.isEmpty()) {
emitFailed(m_preFailure);
return;
}
next();
}
void MinecraftUpdate::next()
{
if (m_abort) {
emitFailed(tr("Aborted by user."));
return;
}
if (m_failed_out_of_order) {
emitFailed(m_fail_reason);
return;
}
m_currentTask++;
if (m_currentTask > 0) {
auto task = m_tasks[m_currentTask - 1];
disconnect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded);
disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed);
disconnect(task.get(), &Task::aborted, this, &Task::abort);
disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress);
disconnect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propagateStepProgress);
disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus);
disconnect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails);
}
if (m_currentTask == m_tasks.size()) {
emitSucceeded();
return;
}
auto task = m_tasks[m_currentTask];
// if the task is already finished by the time we look at it, skip it
if (task->isFinished()) {
qCritical() << "MinecraftUpdate: Skipping finished subtask" << m_currentTask << ":" << task.get();
next();
}
connect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded);
connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed);
connect(task.get(), &Task::aborted, this, &Task::abort);
connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress);
connect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propagateStepProgress);
connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus);
connect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails);
// if the task is already running, do not start it again
if (!task->isRunning()) {
task->start();
}
}
void MinecraftUpdate::subtaskSucceeded()
{
if (isFinished()) {
qCritical() << "MinecraftUpdate: Subtask" << sender() << "succeeded, but work was already done!";
return;
}
auto senderTask = QObject::sender();
auto currentTask = m_tasks[m_currentTask].get();
if (senderTask != currentTask) {
qDebug() << "MinecraftUpdate: Subtask" << sender() << "succeeded out of order.";
return;
}
next();
}
void MinecraftUpdate::subtaskFailed(QString error)
{
if (isFinished()) {
qCritical() << "MinecraftUpdate: Subtask" << sender() << "failed, but work was already done!";
return;
}
auto senderTask = QObject::sender();
auto currentTask = m_tasks[m_currentTask].get();
if (senderTask != currentTask) {
qDebug() << "MinecraftUpdate: Subtask" << sender() << "failed out of order.";
m_failed_out_of_order = true;
m_fail_reason = error;
return;
}
emitFailed(error);
}
bool MinecraftUpdate::abort()
{
if (!m_abort) {
m_abort = true;
auto task = m_tasks[m_currentTask];
if (task->canAbort()) {
return task->abort();
}
}
return true;
}
bool MinecraftUpdate::canAbort() const
{
return true;
SequentialTask::executeTask();
}

View File

@ -15,43 +15,19 @@
#pragma once
#include <QList>
#include <QObject>
#include <QUrl>
#include "tasks/SequentialTask.h"
#include <quazip/quazip.h>
#include "minecraft/VersionFilterData.h"
#include "net/NetJob.h"
#include "tasks/Task.h"
class MinecraftVersion;
class MinecraftInstance;
// FIXME: This looks very similar to a SequentialTask. Maybe we can reduce code duplications? :^)
class MinecraftUpdate : public Task {
// this needs to be a task because components->reload does stuff that may block
class MinecraftUpdate : public SequentialTask {
Q_OBJECT
public:
explicit MinecraftUpdate(MinecraftInstance* inst, QObject* parent = 0);
virtual ~MinecraftUpdate(){};
virtual ~MinecraftUpdate() = default;
void executeTask() override;
bool canAbort() const override;
private slots:
bool abort() override;
void subtaskSucceeded();
void subtaskFailed(QString error);
private:
void next();
private:
MinecraftInstance* m_inst = nullptr;
QList<Task::Ptr> m_tasks;
QString m_preFailure;
int m_currentTask = -1;
bool m_abort = false;
bool m_failed_out_of_order = false;
QString m_fail_reason;
};

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