Merge branch 'develop' of https://github.com/PrismLauncher/PrismLauncher into resource_size

This commit is contained in:
Trial97 2024-05-18 16:53:14 +03:00
commit 477d1408ad
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
257 changed files with 3168 additions and 3863 deletions

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: permissions:
contents: write # for korthout/backport-action to create branch contents: write # for korthout/backport-action to create branch
pull-requests: write # for korthout/backport-action to create PR to backport 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 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)) 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 runs-on: ubuntu-latest
@ -24,7 +25,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs - name: Create backport PRs
uses: korthout/backport-action@v2.1.1 uses: korthout/backport-action@v2.5.0
with: with:
# Config README: https://github.com/korthout/backport-action#backport-action # Config README: https://github.com/korthout/backport-action#backport-action
pull_description: |- pull_description: |-

View File

@ -21,8 +21,23 @@ on:
WINDOWS_CODESIGN_PASSWORD: WINDOWS_CODESIGN_PASSWORD:
description: Password for signing Windows builds description: Password for signing Windows builds
required: false required: false
CACHIX_AUTH_TOKEN: APPLE_CODESIGN_CERT:
description: Private token for authenticating against Cachix cache 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 required: false
GPG_PRIVATE_KEY: GPG_PRIVATE_KEY:
description: Private key for AppImage signing description: Private key for AppImage signing
@ -39,14 +54,17 @@ jobs:
include: include:
- os: ubuntu-20.04 - os: ubuntu-20.04
qt_ver: 5 qt_ver: 5
qt_host: linux
qt_arch: ""
qt_version: "5.12.8"
qt_modules: "qtnetworkauth"
- os: ubuntu-20.04 - os: ubuntu-20.04
qt_ver: 6 qt_ver: 6
qt_host: linux qt_host: linux
qt_arch: "" qt_arch: ""
qt_version: "6.2.4" qt_version: "6.2.4"
qt_modules: "qt5compat qtimageformats" qt_modules: "qt5compat qtimageformats qtnetworkauth"
qt_tools: ""
- os: windows-2022 - os: windows-2022
name: "Windows-MinGW-w64" name: "Windows-MinGW-w64"
@ -60,10 +78,9 @@ jobs:
vcvars_arch: "amd64" vcvars_arch: "amd64"
qt_ver: 6 qt_ver: 6
qt_host: windows qt_host: windows
qt_arch: '' qt_arch: ""
qt_version: '6.6.0' qt_version: "6.7.0"
qt_modules: 'qt5compat qtimageformats' qt_modules: "qt5compat qtimageformats qtnetworkauth"
qt_tools: ''
- os: windows-2022 - os: windows-2022
name: "Windows-MSVC-arm64" name: "Windows-MSVC-arm64"
@ -72,20 +89,18 @@ jobs:
vcvars_arch: "amd64_arm64" vcvars_arch: "amd64_arm64"
qt_ver: 6 qt_ver: 6
qt_host: windows qt_host: windows
qt_arch: 'win64_msvc2019_arm64' qt_arch: "win64_msvc2019_arm64"
qt_version: '6.6.0' qt_version: "6.7.0"
qt_modules: 'qt5compat qtimageformats' qt_modules: "qt5compat qtimageformats qtnetworkauth"
qt_tools: ''
- os: macos-12 - os: macos-12
name: macOS name: macOS
macosx_deployment_target: 11.0 macosx_deployment_target: 11.0
qt_ver: 6 qt_ver: 6
qt_host: mac qt_host: mac
qt_arch: '' qt_arch: ""
qt_version: '6.6.0' qt_version: "6.7.0"
qt_modules: 'qt5compat qtimageformats' qt_modules: "qt5compat qtimageformats qtnetworkauth"
qt_tools: ''
- os: macos-12 - os: macos-12
name: macOS-Legacy name: macOS-Legacy
@ -93,8 +108,7 @@ jobs:
qt_ver: 5 qt_ver: 5
qt_host: mac qt_host: mac
qt_version: "5.15.2" qt_version: "5.15.2"
qt_modules: "" qt_modules: "qtnetworkauth"
qt_tools: ""
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -136,6 +150,7 @@ jobs:
quazip-qt6:p quazip-qt6:p
ccache:p ccache:p
qt6-5compat:p qt6-5compat:p
qt6-networkauth:p
cmark:p cmark:p
- name: Force newer ccache - name: Force newer ccache
@ -145,13 +160,13 @@ jobs:
- name: Setup ccache - name: Setup ccache
if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug'
uses: hendrikmuhs/ccache-action@v1.2.10 uses: hendrikmuhs/ccache-action@v1.2.13
with: with:
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
- name: Retrieve ccache cache (Windows MinGW-w64) - name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
uses: actions/cache@v3.3.2 uses: actions/cache@v4.0.2
with: with:
path: '${{ github.workspace }}\.ccache' path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
@ -192,11 +207,6 @@ jobs:
brew update brew update
brew install ninja extra-cmake-modules 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) - name: Install host Qt (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64' if: runner.os == 'Windows' && matrix.architecture == 'arm64'
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v3
@ -208,20 +218,18 @@ jobs:
target: "desktop" target: "desktop"
arch: "" arch: ""
modules: ${{ matrix.qt_modules }} modules: ${{ matrix.qt_modules }}
tools: ${{ matrix.qt_tools }}
cache: ${{ inputs.is_qt_cached }} cache: ${{ inputs.is_qt_cached }}
cache-key-prefix: host-qt-arm64-windows cache-key-prefix: host-qt-arm64-windows
dir: ${{ github.workspace }}\HostQt dir: ${{ github.workspace }}\HostQt
set-env: false set-env: false
- name: Install Qt (macOS, Linux, Qt 6 & Windows MSVC) - name: Install Qt (macOS, Linux & Windows MSVC)
if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS' || (runner.os == 'Windows' && matrix.msystem == '') if: matrix.msystem == ''
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v3
with: with:
aqtversion: "==3.1.*" aqtversion: "==3.1.*"
py7zrversion: ">=0.20.2" py7zrversion: ">=0.20.2"
version: ${{ matrix.qt_version }} version: ${{ matrix.qt_version }}
host: ${{ matrix.qt_host }}
target: "desktop" target: "desktop"
arch: ${{ matrix.qt_arch }} arch: ${{ matrix.qt_arch }}
modules: ${{ matrix.qt_modules }} modules: ${{ matrix.qt_modules }}
@ -244,7 +252,6 @@ jobs:
wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage" wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage"
${{ github.workspace }}/.github/scripts/prepare_JREs.sh
sudo apt install libopengl0 sudo apt install libopengl0
- name: Add QT_HOST_PATH var (Windows MSVC arm64) - name: Add QT_HOST_PATH var (Windows MSVC arm64)
@ -336,6 +343,20 @@ jobs:
# PACKAGE BUILDS # 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) - name: Package (macOS)
if: runner.os == 'macOS' if: runner.os == 'macOS'
run: | run: |
@ -343,9 +364,34 @@ jobs:
cd ${{ env.INSTALL_DIR }} cd ${{ env.INSTALL_DIR }}
chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher" 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" 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) - name: Make Sparkle signature (macOS)
if: matrix.name == 'macOS' if: matrix.name == 'macOS'
@ -353,7 +399,7 @@ jobs:
if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then
brew install openssl@3 brew install openssl@3
echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem 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 rm ed25519-priv.pem
cat >> $GITHUB_STEP_SUMMARY << EOF cat >> $GITHUB_STEP_SUMMARY << EOF
### Artifact Information :information_source: ### Artifact Information :information_source:
@ -379,12 +425,6 @@ jobs:
run: | run: |
cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }} 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 }} 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 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 ":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) - name: Package AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5 if: runner.os == 'Linux' && matrix.qt_ver != 5
shell: bash shell: bash
@ -472,13 +492,9 @@ jobs:
chmod +x linuxdeploy-*.AppImage 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 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 -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/ 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/ 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"
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 export LD_LIBRARY_PATH
chmod +x AppImageUpdate-x86_64.AppImage 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" 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 # UPLOAD BUILDS
## ##
- name: Upload binary tarball (macOS) - name: Upload binary tarball (macOS)
if: runner.os == 'macOS' if: runner.os == 'macOS'
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher.tar.gz path: PrismLauncher.zip
- name: Upload binary zip (Windows) - name: Upload binary zip (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
path: ${{ env.INSTALL_DIR }}/** path: ${{ env.INSTALL_DIR }}/**
- name: Upload binary zip (Windows, portable) - name: Upload binary zip (Windows, portable)
if: runner.os == 'Windows' if: runner.os == 'Windows'
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ matrix.name }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ matrix.name }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
path: ${{ env.INSTALL_PORTABLE_DIR }}/** path: ${{ env.INSTALL_PORTABLE_DIR }}/**
- name: Upload installer (Windows) - name: Upload installer (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ matrix.name }}-Setup-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ matrix.name }}-Setup-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher-Setup.exe 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) - name: Upload binary tarball (Linux, portable, Qt 5)
if: runner.os == 'Linux' && matrix.qt_ver != 6 if: runner.os == 'Linux' && matrix.qt_ver != 6
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ runner.os }}-Qt5-Portable-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ runner.os }}-Qt5-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher-portable.tar.gz 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) - name: Upload binary tarball (Linux, portable, Qt 6)
if: runner.os == 'Linux' && matrix.qt_ver != 5 if: runner.os == 'Linux' && matrix.qt_ver != 5
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ runner.os }}-Qt6-Portable-${{ env.VERSION }}-${{ inputs.build_type }} name: PrismLauncher-${{ runner.os }}-Qt6-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher-portable.tar.gz path: PrismLauncher-portable.tar.gz
- name: Upload AppImage (Linux) - name: Upload AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5 if: runner.os == 'Linux' && matrix.qt_ver != 5
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
path: 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) - name: Upload AppImage Zsync (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5 if: runner.os == 'Linux' && matrix.qt_ver != 5
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage.zsync name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage.zsync
path: PrismLauncher-Linux-x86_64.AppImage.zsync path: PrismLauncher-Linux-x86_64.AppImage.zsync

View File

@ -13,7 +13,7 @@ jobs:
submodules: 'true' submodules: 'true'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
config-file: ./.github/codeql/codeql-config.yml config-file: ./.github/codeql/codeql-config.yml
queries: security-and-quality queries: security-and-quality
@ -23,7 +23,7 @@ jobs:
run: run:
sudo apt-get -y update 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 - name: Configure and Build
run: | run: |
@ -32,4 +32,4 @@ jobs:
cmake --build build cmake --build build
- name: Perform CodeQL Analysis - 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 }} SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} 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: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}

View File

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

View File

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

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(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 version numbers ########
set(Launcher_VERSION_MAJOR 8) set(Launcher_VERSION_MAJOR 9)
set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_MINOR 0)
set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
@ -282,7 +282,7 @@ endif()
include(QtVersionlessBackport) include(QtVersionlessBackport)
if(Launcher_QT_VERSION_MAJOR EQUAL 5) if(Launcher_QT_VERSION_MAJOR EQUAL 5)
set(QT_VERSION_MAJOR 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) if(NOT Launcher_FORCE_BUNDLED_LIBS)
find_package(QuaZip-Qt5 1.3 QUIET) 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") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE")
elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) elseif(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_VERSION_MAJOR 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) list(APPEND Launcher_QT_LIBS Qt6::Core5Compat)
if(NOT Launcher_FORCE_BUNDLED_LIBS) 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_SHORT_VERSION_STRING "${Launcher_VERSION_NAME}")
set(MACOSX_BUNDLE_LONG_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_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_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_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_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 "bf6ac1caa9f8d321d5784859c88da874f28412f37fb327bc21b7b14c5d61ef94" CACHE STRING "SHA256 checksum for 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") set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle")
# directories to look for dependencies # 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_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR})
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}") 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) if(Launcher_ManPage)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6")
endif() endif()
@ -504,15 +516,13 @@ else()
endif() endif()
if(NOT cmark_FOUND) if(NOT cmark_FOUND)
message(STATUS "Using bundled cmark") message(STATUS "Using bundled cmark")
set(CMARK_STATIC ON CACHE BOOL "Build static libcmark library" FORCE) set(BUILD_TESTING 0)
set(CMARK_SHARED OFF CACHE BOOL "Build shared libcmark library" FORCE) set(BUILD_SHARED_LIBS 0)
set(CMARK_TESTS OFF CACHE BOOL "Build cmark tests and enable testing" FORCE)
add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser
add_library(cmark::cmark ALIAS cmark_static) add_library(cmark::cmark ALIAS cmark)
else() else()
message(STATUS "Using system cmark") message(STATUS "Using system cmark")
endif() endif()
add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much
add_subdirectory(libraries/gamemode) add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
if (NOT ghc_filesystem_FOUND) if (NOT ghc_filesystem_FOUND)

View File

@ -1,7 +1,7 @@
## Prism Launcher ## Prism Launcher
Prism Launcher - Minecraft 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 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 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 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. 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 ## Gamemode
Copyright (c) 2017-2022, Feral Interactive Copyright (c) 2017-2022, Feral Interactive
@ -436,7 +410,7 @@
Copyright (C) 2007 Johann Ollivier Lapeyre <johann@oxygen-icons.org> Copyright (C) 2007 Johann Ollivier Lapeyre <johann@oxygen-icons.org>
Copyright (C) 2007 Kenneth Wimer <kwwii@bootsplash.org> Copyright (C) 2007 Kenneth Wimer <kwwii@bootsplash.org>
Copyright (C) 2007 Riccardo Iaconelli <riccardo@oxygen-icons.org> Copyright (C) 2007 Riccardo Iaconelli <riccardo@oxygen-icons.org>
and others and others
This library is free software; you can redistribute it and/or This library is free software; you can redistribute it and/or

View File

@ -6,6 +6,8 @@
<string>A Minecraft mod wants to access your camera.</string> <string>A Minecraft mod wants to access your camera.</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>A Minecraft mod wants to access your microphone.</string> <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> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>

68
flake.lock generated
View File

@ -18,14 +18,16 @@
}, },
"flake-parts": { "flake-parts": {
"inputs": { "inputs": {
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": [
"nixpkgs"
]
}, },
"locked": { "locked": {
"lastModified": 1698882062, "lastModified": 1714641030,
"narHash": "sha256-HkhafUayIqxXyHH1X8d9RDl1M2CkFgZLjKD3MzabiEo=", "narHash": "sha256-yzcRNDoyVP7+SCNX0wmuDju1NUCt8Dz9+lyUXEI0dbI=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "8c9fa2545007b49a5db5f650ae91f227672c3877", "rev": "e5d10a24b66c3ea8f150e47dfdb0416ab7c3390e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -39,11 +41,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1685518550, "lastModified": 1710146030,
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -60,11 +62,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1660459072, "lastModified": 1709087332,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "gitignore.nix", "repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -89,28 +91,13 @@
"type": "github" "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": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1700108881, "lastModified": 1715413075,
"narHash": "sha256-+Lqybl8kj0+nD/IlAWPPG/RDTa47gff9nbei0u7BntE=", "narHash": "sha256-FCi3R1MeS5bVp0M0xTheveP6hhcCYfW/aghSTPebYL4=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "7414e9ee0b3e9903c24d3379f577a417f0aae5f1", "rev": "e4e7a43a9db7e22613accfeb1005cca1b2b1ee0d",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -120,24 +107,6 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1698611440,
"narHash": "sha256-jPjHjrerhYDy3q9+s5EAsuhyhuknNfowY6yt6pjn9pc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0cbe9f69c234a7700596e943bfae7ef27a31b735",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": { "pre-commit-hooks": {
"inputs": { "inputs": {
"flake-compat": [ "flake-compat": [
@ -153,11 +122,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1700064067, "lastModified": 1714478972,
"narHash": "sha256-1ZWNDzhu8UlVCK7+DUN9dVQfiHX1bv6OQP9VxstY/gs=", "narHash": "sha256-q//cgb52vv81uOuwz1LaXElp3XAe1TqrABXODAEF6Sk=",
"owner": "cachix", "owner": "cachix",
"repo": "pre-commit-hooks.nix", "repo": "pre-commit-hooks.nix",
"rev": "e558068cba67b23b4fbc5537173dbb43748a17e8", "rev": "2849da033884f54822af194400f8dff435ada242",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -171,7 +140,6 @@
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
"libnbtplusplus": "libnbtplusplus", "libnbtplusplus": "libnbtplusplus",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks" "pre-commit-hooks": "pre-commit-hooks"
} }

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)"; 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 = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts"; flake-parts = {
nix-filter.url = "github:numtide/nix-filter"; url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs";
};
pre-commit-hooks = { pre-commit-hooks = {
url = "github:cachix/pre-commit-hooks.nix"; url = "github:cachix/pre-commit-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs"; inputs = {
inputs.nixpkgs-stable.follows = "nixpkgs"; nixpkgs.follows = "nixpkgs";
inputs.flake-compat.follows = "flake-compat"; nixpkgs-stable.follows = "nixpkgs";
flake-compat.follows = "flake-compat";
};
}; };
flake-compat = { flake-compat = {
url = "github:edolstra/flake-compat"; url = "github:edolstra/flake-compat";

View File

@ -1,8 +1,9 @@
id: org.prismlauncher.PrismLauncher id: org.prismlauncher.PrismLauncher
runtime: org.kde.Platform runtime: org.kde.Platform
runtime-version: "5.15-23.08" runtime-version: 5.15-23.08
sdk: org.kde.Sdk sdk: org.kde.Sdk
sdk-extensions: sdk-extensions:
- org.freedesktop.Sdk.Extension.openjdk21
- org.freedesktop.Sdk.Extension.openjdk17 - org.freedesktop.Sdk.Extension.openjdk17
- org.freedesktop.Sdk.Extension.openjdk8 - org.freedesktop.Sdk.Extension.openjdk8
@ -50,6 +51,8 @@ modules:
buildsystem: simple buildsystem: simple
build-commands: build-commands:
- mkdir -p /app/jdk/ - mkdir -p /app/jdk/
- /usr/lib/sdk/openjdk21/install.sh
- mv /app/jre /app/jdk/21
- /usr/lib/sdk/openjdk17/install.sh - /usr/lib/sdk/openjdk17/install.sh
- mv /app/jre /app/jdk/17 - mv /app/jre /app/jdk/17
- /usr/lib/sdk/openjdk8/install.sh - /usr/lib/sdk/openjdk8/install.sh
@ -104,18 +107,15 @@ modules:
- install -Dm755 ../data/gamemoderun -t /app/bin - install -Dm755 ../data/gamemoderun -t /app/bin
sources: sources:
- type: archive - type: archive
archive-type: tar-gzip dest-filename: gamemode.tar.gz
url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.7 url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.8.1
sha256: 57ce73ba605d1cf12f8d13725006a895182308d93eba0f69f285648449641803 sha256: 969cf85b5ca3944f3e315cd73a0ee9bea4f9c968cd7d485e9f4745bc1e679c4e
x-checker-data: x-checker-data:
type: json type: json
url: https://api.github.com/repos/FeralInteractive/gamemode/releases/latest url: https://api.github.com/repos/FeralInteractive/gamemode/releases/latest
version-query: .tag_name version-query: .tag_name
url-query: .tarball_url url-query: .tarball_url
timestamp-query: .published_at timestamp-query: .published_at
# from https://github.com/flathub/net.gaijin.WarThunder/blob/7ea6f7a9f84b9c77150c003a7059dc03f8dcbc7f/gamemode.patch
- type: patch
path: patches/gamemode.patch
cleanup: cleanup:
- /include - /include
- /lib/pkgconfig - /lib/pkgconfig

View File

@ -1,12 +0,0 @@
diff -ruN a/common/common-pidfds.c b/common/common-pidfds.c
--- a/common/common-pidfds.c 2021-02-18 20:00:12.000000000 +0100
+++ b/common/common-pidfds.c 2023-09-07 08:57:42.954362763 +0200
@@ -58,6 +58,8 @@
{
return (int)syscall(__NR_pidfd_open, pid, flags);
}
+#else
+#include <sys/pidfd.h>
#endif
/* pidfd functions */

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

View File

@ -48,6 +48,7 @@
#include "pathmatcher/MultiMatcher.h" #include "pathmatcher/MultiMatcher.h"
#include "pathmatcher/SimplePrefixMatcher.h" #include "pathmatcher/SimplePrefixMatcher.h"
#include "settings/INIFile.h" #include "settings/INIFile.h"
#include "tools/GenericProfiler.h"
#include "ui/InstanceWindow.h" #include "ui/InstanceWindow.h"
#include "ui/MainWindow.h" #include "ui/MainWindow.h"
@ -132,6 +133,15 @@
#include "gamemode_client.h" #include "gamemode_client.h"
#endif #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(Q_OS_MAC)
#if defined(SPARKLE_ENABLED) #if defined(SPARKLE_ENABLED)
#include "updater/MacSparkleUpdater.h" #include "updater/MacSparkleUpdater.h"
@ -216,6 +226,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// Don't quit on hiding the last window // Don't quit on hiding the last window
this->setQuitOnLastWindowClosed(false); this->setQuitOnLastWindowClosed(false);
this->setQuitLockEnabled(false);
// Commandline parsing // Commandline parsing
QCommandLineParser parser; QCommandLineParser parser;
@ -299,7 +310,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
adjustedBy = "Persistent data path"; adjustedBy = "Persistent data path";
#ifndef Q_OS_MACOS #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; dataPath = m_rootPath;
adjustedBy = "Portable data path"; adjustedBy = "Portable data path";
m_portable = true; m_portable = true;
@ -485,8 +500,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
} }
{ {
qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME) << ", (c) 2022-2023 " qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
<< qPrintable(QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
qDebug() << "Version : " << BuildConfig.printableVersionString(); qDebug() << "Version : " << BuildConfig.printableVersionString();
qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM; qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM;
qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
@ -631,10 +645,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("UseNativeGLFW", false); m_settings->registerSetting("UseNativeGLFW", false);
m_settings->registerSetting("CustomGLFWPath", ""); m_settings->registerSetting("CustomGLFWPath", "");
// Peformance related options // Performance related options
m_settings->registerSetting("EnableFeralGamemode", false); m_settings->registerSetting("EnableFeralGamemode", false);
m_settings->registerSetting("EnableMangoHud", false); m_settings->registerSetting("EnableMangoHud", false);
m_settings->registerSetting("UseDiscreteGpu", false); m_settings->registerSetting("UseDiscreteGpu", false);
m_settings->registerSetting("UseZink", false);
// Game time // Game time
m_settings->registerSetting("ShowGameTime", true); m_settings->registerSetting("ShowGameTime", true);
@ -658,6 +673,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// The cat // The cat
m_settings->registerSetting("TheCat", false); m_settings->registerSetting("TheCat", false);
m_settings->registerSetting("CatOpacity", 100);
m_settings->registerSetting("StatusBarVisible", true);
m_settings->registerSetting("ToolbarsLocked", false); m_settings->registerSetting("ToolbarsLocked", false);
@ -740,6 +758,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("ModrinthToken", ""); m_settings->registerSetting("ModrinthToken", "");
m_settings->registerSetting("UserAgentOverride", ""); m_settings->registerSetting("UserAgentOverride", "");
// FTBApp instances
m_settings->registerSetting("FTBAppInstancesPath", "");
// Init page provider // Init page provider
{ {
m_globalSettingsProvider = std::make_shared<GenericPageProvider>(tr("Settings")); m_globalSettingsProvider = std::make_shared<GenericPageProvider>(tr("Settings"));
@ -859,6 +880,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// FIXME: what to do with these? // FIXME: what to do with these?
m_profilers.insert("jprofiler", std::shared_ptr<BaseProfilerFactory>(new JProfilerFactory())); m_profilers.insert("jprofiler", std::shared_ptr<BaseProfilerFactory>(new JProfilerFactory()));
m_profilers.insert("jvisualvm", std::shared_ptr<BaseProfilerFactory>(new JVisualVMFactory())); 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()) { for (auto profiler : m_profilers.values()) {
profiler->registerSettings(m_settings); profiler->registerSettings(m_settings);
} }
@ -988,6 +1010,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()) { if (createSetupWizard()) {
return; return;
} }
@ -1471,6 +1524,17 @@ InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString pa
auto& window = extras.window; auto& window = extras.window;
if (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->raise();
window->activateWindow(); window->activateWindow();
} else { } else {
@ -1478,6 +1542,7 @@ InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString pa
m_openWindows++; m_openWindows++;
connect(window, &InstanceWindow::isClosing, this, &Application::on_windowClose); connect(window, &InstanceWindow::isClosing, this, &Application::on_windowClose);
} }
if (!page.isEmpty()) { if (!page.isEmpty()) {
window->selectPage(page); window->selectPage(page);
} }

View File

@ -64,6 +64,8 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s
m_settings->registerSetting("lastLaunchTime", 0); m_settings->registerSetting("lastLaunchTime", 0);
m_settings->registerSetting("totalTimePlayed", 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("lastTimePlayed", 0);
m_settings->registerSetting("linkedInstances", "[]"); m_settings->registerSetting("linkedInstances", "[]");

View File

@ -126,7 +126,6 @@ set(NET_SOURCES
net/MetaCacheSink.h net/MetaCacheSink.h
net/Logging.h net/Logging.h
net/Logging.cpp net/Logging.cpp
net/NetAction.h
net/NetJob.cpp net/NetJob.cpp
net/NetJob.h net/NetJob.h
net/NetUtils.h net/NetUtils.h
@ -210,28 +209,17 @@ set(MINECRAFT_SOURCES
minecraft/auth/AccountData.h minecraft/auth/AccountData.h
minecraft/auth/AccountList.cpp minecraft/auth/AccountList.cpp
minecraft/auth/AccountList.h 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.cpp
minecraft/auth/AuthSession.h minecraft/auth/AuthSession.h
minecraft/auth/AuthStep.cpp
minecraft/auth/AuthStep.h minecraft/auth/AuthStep.h
minecraft/auth/MinecraftAccount.cpp minecraft/auth/MinecraftAccount.cpp
minecraft/auth/MinecraftAccount.h minecraft/auth/MinecraftAccount.h
minecraft/auth/Parsers.cpp minecraft/auth/Parsers.cpp
minecraft/auth/Parsers.h minecraft/auth/Parsers.h
minecraft/auth/flows/AuthFlow.cpp minecraft/auth/AuthFlow.cpp
minecraft/auth/flows/AuthFlow.h minecraft/auth/AuthFlow.h
minecraft/auth/flows/MSA.cpp
minecraft/auth/flows/MSA.h
minecraft/auth/flows/Offline.cpp
minecraft/auth/flows/Offline.h
minecraft/auth/steps/OfflineStep.cpp
minecraft/auth/steps/OfflineStep.h
minecraft/auth/steps/EntitlementsStep.cpp minecraft/auth/steps/EntitlementsStep.cpp
minecraft/auth/steps/EntitlementsStep.h minecraft/auth/steps/EntitlementsStep.h
minecraft/auth/steps/GetSkinStep.cpp minecraft/auth/steps/GetSkinStep.cpp
@ -240,6 +228,8 @@ set(MINECRAFT_SOURCES
minecraft/auth/steps/LauncherLoginStep.h minecraft/auth/steps/LauncherLoginStep.h
minecraft/auth/steps/MinecraftProfileStep.cpp minecraft/auth/steps/MinecraftProfileStep.cpp
minecraft/auth/steps/MinecraftProfileStep.h minecraft/auth/steps/MinecraftProfileStep.h
minecraft/auth/steps/MSADeviceCodeStep.cpp
minecraft/auth/steps/MSADeviceCodeStep.h
minecraft/auth/steps/MSAStep.cpp minecraft/auth/steps/MSAStep.cpp
minecraft/auth/steps/MSAStep.h minecraft/auth/steps/MSAStep.h
minecraft/auth/steps/XboxAuthorizationStep.cpp minecraft/auth/steps/XboxAuthorizationStep.cpp
@ -453,6 +443,8 @@ set(TOOLS_SOURCES
tools/JVisualVM.h tools/JVisualVM.h
tools/MCEditTool.cpp tools/MCEditTool.cpp
tools/MCEditTool.h tools/MCEditTool.h
tools/GenericProfiler.cpp
tools/GenericProfiler.h
) )
set(META_SOURCES set(META_SOURCES
@ -624,7 +616,6 @@ set(PRISMUPDATER_SOURCES
net/HttpMetaCache.h net/HttpMetaCache.h
net/Logging.h net/Logging.h
net/Logging.cpp net/Logging.cpp
net/NetAction.h
net/NetRequest.cpp net/NetRequest.cpp
net/NetRequest.h net/NetRequest.h
net/NetJob.cpp net/NetJob.cpp
@ -827,6 +818,8 @@ SET(LAUNCHER_SOURCES
ui/themes/DarkTheme.h ui/themes/DarkTheme.h
ui/themes/ITheme.cpp ui/themes/ITheme.cpp
ui/themes/ITheme.h ui/themes/ITheme.h
ui/themes/HintOverrideProxyStyle.cpp
ui/themes/HintOverrideProxyStyle.h
ui/themes/SystemTheme.cpp ui/themes/SystemTheme.cpp
ui/themes/SystemTheme.h ui/themes/SystemTheme.h
ui/themes/IconTheme.cpp ui/themes/IconTheme.cpp
@ -1239,7 +1232,6 @@ target_link_libraries(Launcher_logic
tomlplusplus::tomlplusplus tomlplusplus::tomlplusplus
qdcss qdcss
BuildConfig BuildConfig
Katabasis
Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Widgets
ghcFilesystem::ghc_filesystem ghcFilesystem::ghc_filesystem
) )
@ -1257,6 +1249,7 @@ target_link_libraries(Launcher_logic
Qt${QT_VERSION_MAJOR}::Concurrent Qt${QT_VERSION_MAJOR}::Concurrent
Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::NetworkAuth
${Launcher_QT_LIBS} ${Launcher_QT_LIBS}
) )
target_link_libraries(Launcher_logic target_link_libraries(Launcher_logic
@ -1327,7 +1320,6 @@ if(Launcher_BUILD_UPDATER)
Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Network
${Launcher_QT_LIBS} ${Launcher_QT_LIBS}
cmark::cmark cmark::cmark
Katabasis
) )
add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp) add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp)
@ -1492,7 +1484,6 @@ if(INSTALL_BUNDLE STREQUAL "full")
CONFIGURATIONS Debug RelWithDebInfo "" CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR} DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime COMPONENT Runtime
PATTERN "*qopensslbackend*" EXCLUDE
PATTERN "*qcertonlybackend*" EXCLUDE PATTERN "*qcertonlybackend*" EXCLUDE
) )
install( install(
@ -1503,10 +1494,78 @@ if(INSTALL_BUNDLE STREQUAL "full")
REGEX "dd\\." EXCLUDE REGEX "dd\\." EXCLUDE
REGEX "_debug\\." EXCLUDE REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE REGEX "\\.dSYM" EXCLUDE
PATTERN "*qopensslbackend*" EXCLUDE
PATTERN "*qcertonlybackend*" EXCLUDE PATTERN "*qcertonlybackend*" EXCLUDE
) )
endif() 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( configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in" "${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake"

View File

@ -37,140 +37,33 @@
#include <QDesktopServices> #include <QDesktopServices>
#include <QDir> #include <QDir>
#include <QProcess> #include <QProcess>
#include "FileSystem.h"
/**
* 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
namespace DesktopServices { namespace DesktopServices {
bool openDirectory(const QString& path, [[maybe_unused]] bool ensureExists) bool openPath(const QFileInfo& path, bool ensureFolderPathExists)
{ {
qDebug() << "Opening directory" << path; qDebug() << "Opening path" << path;
QDir parentPath; if (ensureFolderPathExists) {
QDir dir(path); FS::ensureFolderPathExists(path);
if (ensureExists && !dir.exists()) {
parentPath.mkpath(dir.absolutePath());
} }
auto f = [&]() { return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath())); }; return openUrl(QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath()));
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if (!isSandbox()) {
return IndirectOpen(f);
}
#endif
return f();
} }
bool openFile(const QString& path) bool openPath(const QString& path, bool ensureFolderPathExists)
{ {
qDebug() << "Opening file" << path; return openPath(QFileInfo(path), ensureFolderPathExists);
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
} }
bool run(const QString& application, const QStringList& args, const QString& workingDirectory, qint64* pid) bool run(const QString& application, const QStringList& args, const QString& workingDirectory, qint64* pid)
{ {
qDebug() << "Running" << application << "with args" << args.join(' '); 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); return QProcess::startDetached(application, args, workingDirectory, pid);
#endif
} }
bool openUrl(const QUrl& url) bool openUrl(const QUrl& url)
{ {
qDebug() << "Opening URL" << url.toString(); qDebug() << "Opening URL" << url.toString();
auto f = [&]() { return QDesktopServices::openUrl(url); }; 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
} }
bool isFlatpak() bool isFlatpak()
@ -191,9 +84,4 @@ bool isSnap()
#endif #endif
} }
bool isSandbox()
{
return isSnap() || isFlatpak();
}
} // namespace DesktopServices } // namespace DesktopServices

View File

@ -3,31 +3,30 @@
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
class QFileInfo;
/** /**
* This wraps around QDesktopServices and adds workarounds where needed * This wraps around QDesktopServices and adds workarounds where needed
* Use this instead of QDesktopServices! * Use this instead of QDesktopServices!
*/ */
namespace DesktopServices { 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 * Run an application
*/ */
bool run(const QString& application, const QStringList& args, const QString& workingDirectory = QString(), qint64* pid = 0); 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. * 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 * Determine whether the launcher is running in a Snap environment
*/ */
bool isSnap(); bool isSnap();
/**
* Determine whether the launcher is running in a sandboxed (Flatpak or Snap) environment
*/
bool isSandbox();
} // namespace DesktopServices } // 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 #pragma once
@ -8,12 +41,12 @@
class Exception : public std::exception { class Exception : public std::exception {
public: public:
Exception(const QString& message) : std::exception(), m_message(message) { qCritical() << "Exception:" << message; } Exception(const QString& message) : std::exception(), m_message(message.toUtf8()) { qCritical() << "Exception:" << message; }
Exception(const Exception& other) : std::exception(), m_message(other.cause()) {} Exception(const Exception& other) : std::exception(), m_message(other.m_message) {}
virtual ~Exception() noexcept {} virtual ~Exception() noexcept {}
const char* what() const noexcept { return m_message.toLatin1().constData(); } const char* what() const noexcept { return m_message.constData(); }
QString cause() const { return m_message; } QString cause() const { return QString::fromUtf8(m_message); }
private: private:
QString m_message; QByteArray m_message;
}; };

View File

@ -272,15 +272,19 @@ bool ensureFilePathExists(QString filenamepath)
return success; return success;
} }
bool ensureFolderPathExists(QString foldernamepath) bool ensureFolderPathExists(const QFileInfo folderPath)
{ {
QFileInfo a(foldernamepath);
QDir dir; QDir dir;
QString ensuredPath = a.filePath(); QString ensuredPath = folderPath.filePath();
bool success = dir.mkpath(ensuredPath); bool success = dir.mkpath(ensuredPath);
return success; return success;
} }
bool ensureFolderPathExists(const QString folderPathName)
{
return ensureFolderPathExists(QFileInfo(folderPathName));
}
bool copyFileAttributes(QString src, QString dst) bool copyFileAttributes(QString src, QString dst)
{ {
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
@ -797,15 +801,24 @@ QString NormalizePath(QString path)
} }
} }
QString badFilenameChars = "\"\\/?<>:;*|!+\r\n"; static const QString BAD_PATH_CHARS = "\"?<>:;*|!+\r\n";
static const QString BAD_FILENAME_CHARS = BAD_PATH_CHARS + "\\/";
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{ {
for (int i = 0; i < string.length(); i++) { for (int i = 0; i < string.length(); i++)
if (badFilenameChars.contains(string[i])) { if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i)))
string[i] = replaceWith; string[i] = replaceWith;
}
} return string;
}
QString RemoveInvalidPathChars(QString string, QChar replaceWith)
{
for (int i = 0; i < string.length(); i++)
if (string.at(i) < ' ' || BAD_PATH_CHARS.contains(string.at(i)))
string[i] = replaceWith;
return string; return string;
} }
@ -1581,4 +1594,44 @@ uintmax_t hardLinkCount(const QString& path)
return count; 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
} // namespace FS } // namespace FS

View File

@ -91,7 +91,13 @@ bool ensureFilePathExists(QString filenamepath);
* Creates all the folders in a path for the specified path * 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! * 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 * @brief Copies a directory and it's contents from src to dest
@ -336,6 +342,8 @@ QString NormalizePath(QString path);
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-'); QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-');
QString RemoveInvalidPathChars(QString string, QChar replaceWith = '-');
QString DirNameFromString(QString string, QString inDir = "."); QString DirNameFromString(QString string, QString inDir = ".");
/// Checks if the a given Path contains "!" /// Checks if the a given Path contains "!"
@ -545,4 +553,8 @@ bool canLink(const QString& src, const QString& dst);
uintmax_t hardLinkCount(const QString& path); uintmax_t hardLinkCount(const QString& path);
#ifdef Q_OS_WIN
QString getPathNameInLocal8bit(const QString& file);
#endif
} // namespace FS } // namespace FS

View File

@ -43,10 +43,10 @@ void InstanceCopyTask::executeTask()
QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft")); QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
QString staging_mc_dir; QString staging_mc_dir;
if (mcDir.exists() && !dotMCDir.exists()) if (dotMCDir.exists() && !mcDir.exists())
staging_mc_dir = mcDir.filePath();
else
staging_mc_dir = dotMCDir.filePath(); staging_mc_dir = dotMCDir.filePath();
else
staging_mc_dir = mcDir.filePath();
FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves")); FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves"));
savesCopy.followSymlinks(true); savesCopy.followSymlinks(true);
@ -142,9 +142,8 @@ void InstanceCopyTask::copyFinished()
if (!m_keepPlaytime) { if (!m_keepPlaytime) {
inst->resetTimePlayed(); inst->resetTimePlayed();
} }
if (m_useLinks)
inst->addLinkedInstanceId(m_origInstance->id());
if (m_useLinks) { if (m_useLinks) {
inst->addLinkedInstanceId(m_origInstance->id());
auto allowed_symlinks_file = QFileInfo(FS::PathCombine(inst->gameRoot(), "allowed_symlinks.txt")); auto allowed_symlinks_file = QFileInfo(FS::PathCombine(inst->gameRoot(), "allowed_symlinks.txt"));
QByteArray allowed_symlinks; QByteArray allowed_symlinks;

View File

@ -3,8 +3,6 @@
#include <QDebug> #include <QDebug>
#include <QFile> #include <QFile>
InstanceCreationTask::InstanceCreationTask() = default;
void InstanceCreationTask::executeTask() void InstanceCreationTask::executeTask()
{ {
setAbortable(true); setAbortable(true);

View File

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

View File

@ -164,8 +164,8 @@ void InstanceImportTask::processZipPack()
} else if (technicFound) { } else if (technicFound) {
// process as Technic pack // process as Technic pack
qDebug() << "Technic:" << technicFound; qDebug() << "Technic:" << technicFound;
extractDir.mkpath(".minecraft"); extractDir.mkpath("minecraft");
extractDir.cd(".minecraft"); extractDir.cd("minecraft");
m_modpackType = ModpackType::Technic; m_modpackType = ModpackType::Technic;
} else { } else {
QStringList paths_to_ignore{ "overrides/" }; QStringList paths_to_ignore{ "overrides/" };

View File

@ -47,9 +47,6 @@
#include <optional> #include <optional>
class QuaZip; class QuaZip;
namespace Flame {
class FileResolvingTask;
}
class InstanceImportTask : public InstanceTask { class InstanceImportTask : public InstanceTask {
Q_OBJECT Q_OBJECT
@ -79,7 +76,6 @@ class InstanceImportTask : public InstanceTask {
private: /* data */ private: /* data */
NetJob::Ptr m_filesNetJob; NetJob::Ptr m_filesNetJob;
shared_qobject_ptr<Flame::FileResolvingTask> m_modIdResolver;
QUrl m_sourceUrl; QUrl m_sourceUrl;
QString m_archivePath; QString m_archivePath;
bool m_downloadRequired = false; bool m_downloadRequired = false;

View File

@ -38,6 +38,7 @@
#include <QDir> #include <QDir>
#include <QDirIterator> #include <QDirIterator>
#include <QFile> #include <QFile>
#include <QFileInfo>
#include <QFileSystemWatcher> #include <QFileSystemWatcher>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
@ -847,14 +848,16 @@ class InstanceStaging : public Task {
const unsigned maxBackoff = 16; const unsigned maxBackoff = 16;
public: public:
InstanceStaging(InstanceList* parent, InstanceTask* child, QString stagingPath, InstanceName const& instanceName, QString groupName) InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObjectPtr settings)
: m_parent(parent) : m_parent(parent), backoff(minBackoff, maxBackoff)
, backoff(minBackoff, maxBackoff)
, m_stagingPath(std::move(stagingPath))
, m_instance_name(std::move(instanceName))
, m_groupName(std::move(groupName))
{ {
m_stagingPath = parent->getStagedInstancePath();
m_child.reset(child); 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::succeeded, this, &InstanceStaging::childSucceeded);
connect(child, &Task::failed, this, &InstanceStaging::childFailed); connect(child, &Task::failed, this, &InstanceStaging::childFailed);
connect(child, &Task::aborted, this, &InstanceStaging::childAborted); connect(child, &Task::aborted, this, &InstanceStaging::childAborted);
@ -866,7 +869,7 @@ class InstanceStaging : public Task {
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded); connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded);
} }
virtual ~InstanceStaging(){}; virtual ~InstanceStaging() {}
// FIXME/TODO: add ability to abort during instance commit retries // FIXME/TODO: add ability to abort during instance commit retries
bool abort() override bool abort() override
@ -881,14 +884,22 @@ class InstanceStaging : public Task {
bool canAbort() const override { return (m_child && m_child->canAbort()); } bool canAbort() const override { return (m_child && m_child->canAbort()); }
protected: 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(); } QStringList warnings() const override { return m_child->warnings(); }
private slots: private slots:
void childSucceeded() void childSucceeded()
{ {
unsigned sleepTime = backoff(); 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(); emitSucceeded();
return; return;
} }
@ -897,7 +908,7 @@ class InstanceStaging : public Task {
emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something.")); emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something."));
return; return;
} }
qDebug() << "Failed to commit instance" << m_instance_name.name() << "Initiating backoff:" << sleepTime; qDebug() << "Failed to commit instance" << m_child->name() << "Initiating backoff:" << sleepTime;
m_backoffTimer.start(sleepTime * 500); m_backoffTimer.start(sleepTime * 500);
} }
void childFailed(const QString& reason) void childFailed(const QString& reason)
@ -906,7 +917,11 @@ class InstanceStaging : public Task {
emitFailed(reason); emitFailed(reason);
} }
void childAborted() { emitAborted(); } void childAborted()
{
m_parent->destroyStagingPath(m_stagingPath);
emitAborted();
}
private: private:
InstanceList* m_parent; InstanceList* m_parent;
@ -918,34 +933,35 @@ class InstanceStaging : public Task {
ExponentialSeries backoff; ExponentialSeries backoff;
QString m_stagingPath; QString m_stagingPath;
unique_qobject_ptr<InstanceTask> m_child; unique_qobject_ptr<InstanceTask> m_child;
InstanceName m_instance_name;
QString m_groupName;
QTimer m_backoffTimer; QTimer m_backoffTimer;
}; };
Task* InstanceList::wrapInstanceTask(InstanceTask* task) Task* InstanceList::wrapInstanceTask(InstanceTask* task)
{ {
auto stagingPath = getStagedInstancePath(); return new InstanceStaging(this, task, m_globalSettings);
task->setStagingPath(stagingPath);
task->setParentSettings(m_globalSettings);
return new InstanceStaging(this, task, stagingPath, *task, task->group());
} }
QString InstanceList::getStagedInstancePath() QString InstanceList::getStagedInstancePath()
{ {
QString key = QUuid::createUuid().toString(QUuid::WithoutBraces); const QString tempRoot = FS::PathCombine(m_instDir, ".tmp");
QString tempDir = ".LAUNCHER_TEMP/";
QString relPath = FS::PathCombine(tempDir, key); QString result;
QDir rootPath(m_instDir); int tries = 0;
auto path = FS::PathCombine(m_instDir, relPath);
if (!rootPath.mkpath(relPath)) { do {
return QString(); 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 #ifdef Q_OS_WIN32
auto tempPath = FS::PathCombine(m_instDir, tempDir); SetFileAttributesA(tempRoot.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
SetFileAttributesA(tempPath.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
#endif #endif
return path; return result;
} }
bool InstanceList::commitStagedInstance(const QString& path, bool InstanceList::commitStagedInstance(const QString& path,

View File

@ -2,6 +2,8 @@
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include <QPushButton>
InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name) InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name)
{ {
auto dialog = auto dialog =
@ -27,16 +29,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 " "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).") "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).")
.arg(original_version_name), .arg(original_version_name),
QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort); QMessageBox::Information, QMessageBox::Cancel);
info->setButtonText(QMessageBox::Ok, QObject::tr("Update existing instance")); QAbstractButton* update = info->addButton(QObject::tr("Update existing instance"), QMessageBox::AcceptRole);
info->setButtonText(QMessageBox::Abort, QObject::tr("Create new instance")); QAbstractButton* skip = info->addButton(QObject::tr("Create new instance"), QMessageBox::ResetRole);
info->setButtonText(QMessageBox::Reset, QObject::tr("Cancel"));
info->exec(); info->exec();
if (info->clickedButton() == info->button(QMessageBox::Ok)) if (info->clickedButton() == update)
return ShouldUpdate::Update; return ShouldUpdate::Update;
if (info->clickedButton() == info->button(QMessageBox::Abort)) if (info->clickedButton() == skip)
return ShouldUpdate::SkipUpdating; return ShouldUpdate::SkipUpdating;
return ShouldUpdate::Cancel; return ShouldUpdate::Cancel;
} }

View File

@ -42,7 +42,6 @@
#include "ui/InstanceWindow.h" #include "ui/InstanceWindow.h"
#include "ui/MainWindow.h" #include "ui/MainWindow.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/EditAccountDialog.h"
#include "ui/dialogs/ProfileSelectDialog.h" #include "ui/dialogs/ProfileSelectDialog.h"
#include "ui/dialogs/ProfileSetupDialog.h" #include "ui/dialogs/ProfileSetupDialog.h"
#include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ProgressDialog.h"
@ -58,7 +57,6 @@
#include "BuildConfig.h" #include "BuildConfig.h"
#include "JavaCommon.h" #include "JavaCommon.h"
#include "launch/steps/TextPrint.h" #include "launch/steps/TextPrint.h"
#include "minecraft/auth/AccountTask.h"
#include "tasks/Task.h" #include "tasks/Task.h"
LaunchController::LaunchController(QObject* parent) : Task(parent) {} LaunchController::LaunchController(QObject* parent) : Task(parent) {}
@ -144,6 +142,12 @@ void LaunchController::login()
bool tryagain = true; bool tryagain = true;
unsigned int tries = 0; unsigned int tries = 0;
if (m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) {
// 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) { while (tryagain) {
if (tries > 0 && tries % 3 == 0) { if (tries > 0 && tries % 3 == 0) {
auto result = auto result =
@ -250,12 +254,6 @@ void LaunchController::login()
progDialog.execWithTask(task.get()); progDialog.execWithTask(task.get());
continue; 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: { case AccountState::Expired: {
auto errorString = tr("The account has expired and needs to be logged into manually again."); 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, QMessageBox::warning(m_parentWidget, tr("Account refresh failed"), errorString, QMessageBox::StandardButton::Ok,

View File

@ -119,6 +119,7 @@ bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool follow
bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks) bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks)
{ {
QuaZip zip(fileCompressed); QuaZip zip(fileCompressed);
zip.setUtf8Enabled(true);
QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
if (!zip.open(QuaZip::mdCreate)) { if (!zip.open(QuaZip::mdCreate)) {
QFile::remove(fileCompressed); QFile::remove(fileCompressed);
@ -141,6 +142,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files,
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods) bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods)
{ {
QuaZip zipOut(targetJarPath); QuaZip zipOut(targetJarPath);
zipOut.setUtf8Enabled(true);
if (!zipOut.open(QuaZip::mdCreate)) { if (!zipOut.open(QuaZip::mdCreate)) {
QFile::remove(targetJarPath); QFile::remove(targetJarPath);
qCritical() << "Failed to open the minecraft.jar for modding"; qCritical() << "Failed to open the minecraft.jar for modding";
@ -286,10 +288,13 @@ std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, con
do { do {
QString file_name = zip->getCurrentFileName(); QString file_name = zip->getCurrentFileName();
#ifdef Q_OS_WIN
file_name = FS::RemoveInvalidPathChars(file_name);
#endif
if (!file_name.startsWith(subdir)) if (!file_name.startsWith(subdir))
continue; 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; auto original_name = relative_file_name;
// Fix subdirs/files ending with a / getting transformed into absolute paths // Fix subdirs/files ending with a / getting transformed into absolute paths
@ -463,7 +468,7 @@ auto ExportToZipTask::exportZip() -> ZipResult
auto absolute = file.absoluteFilePath(); auto absolute = file.absoluteFilePath();
auto relative = m_dir.relativeFilePath(absolute); auto relative = m_dir.relativeFilePath(absolute);
setStatus("Compresing: " + relative); setStatus("Compressing: " + relative);
setProgress(m_progress + 1, m_progressTotal); setProgress(m_progress + 1, m_progressTotal);
if (m_follow_symlinks) { if (m_follow_symlinks) {
if (file.isSymLink()) if (file.isSymLink())

View File

@ -154,7 +154,12 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q
#if defined(LAUNCHER_APPLICATION) #if defined(LAUNCHER_APPLICATION)
class ExportToZipTask : public Task { class ExportToZipTask : public Task {
public: 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_path(outputPath)
, m_output(outputPath) , m_output(outputPath)
, m_dir(dir) , m_dir(dir)
@ -163,9 +168,15 @@ class ExportToZipTask : public Task {
, m_follow_symlinks(followSymlinks) , m_follow_symlinks(followSymlinks)
{ {
setAbortable(true); setAbortable(true);
m_output.setUtf8Enabled(utf8Enabled);
}; };
ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) ExportToZipTask(QString outputPath,
: ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks){}; QString dir,
QFileInfoList files,
QString destinationPrefix = "",
bool followSymlinks = false,
bool utf8Enabled = false)
: ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled){};
virtual ~ExportToZipTask() = default; virtual ~ExportToZipTask() = default;

View File

@ -1,5 +1,4 @@
set(CMAKE_MODULE_PATH "@CMAKE_MODULE_PATH@") set(CMAKE_MODULE_PATH "@CMAKE_MODULE_PATH@")
file(GLOB_RECURSE QTPLUGINS "${CMAKE_INSTALL_PREFIX}/@PLUGIN_DEST_DIR@/*@CMAKE_SHARED_LIBRARY_SUFFIX@") file(GLOB_RECURSE QTPLUGINS "${CMAKE_INSTALL_PREFIX}/@PLUGIN_DEST_DIR@/*@CMAKE_SHARED_LIBRARY_SUFFIX@")
function(gp_resolved_file_type_override resolved_file type_var) function(gp_resolved_file_type_override resolved_file type_var)
if(resolved_file MATCHES "^/(usr/)?lib/libQt") 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."; qDebug() << "Java checker library could not be found. Please check your installation.";
return; return;
} }
#ifdef Q_OS_WIN
checkerJar = FS::getPathNameInLocal8bit(checkerJar);
#endif
QStringList args; QStringList args;

View File

@ -362,6 +362,12 @@ QList<QString> JavaUtils::FindJavaPaths()
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java");
} }
auto home = qEnvironmentVariable("HOME");
// javas downloaded by sdkman
javas.append(FS::PathCombine(home, ".sdkman/candidates/java"));
javas.append(getMinecraftJavaBundle()); javas.append(getMinecraftJavaBundle());
javas = addJavasFromEnv(javas); javas = addJavasFromEnv(javas);
javas.removeDuplicates(); javas.removeDuplicates();
@ -413,6 +419,8 @@ QList<QString> JavaUtils::FindJavaPaths()
scanJavaDirs(FS::PathCombine(home, ".jdks")); scanJavaDirs(FS::PathCombine(home, ".jdks"));
// javas downloaded by sdkman // javas downloaded by sdkman
scanJavaDirs(FS::PathCombine(home, ".sdkman/candidates/java")); scanJavaDirs(FS::PathCombine(home, ".sdkman/candidates/java"));
// javas downloaded by gradle (toolchains)
scanJavaDirs(FS::PathCombine(home, ".gradle/jdks"));
javas.append(getMinecraftJavaBundle()); javas.append(getMinecraftJavaBundle());
javas = addJavasFromEnv(javas); javas = addJavasFromEnv(javas);
@ -439,26 +447,25 @@ QString JavaUtils::getJavaCheckPath()
QStringList getMinecraftJavaBundle() QStringList getMinecraftJavaBundle()
{ {
QString partialPath;
QString executable = "java"; QString executable = "java";
QStringList processpaths; QStringList processpaths;
#if defined(Q_OS_OSX) #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) #elif defined(Q_OS_WIN32)
partialPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", "");
executable += "w.exe"; 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: // 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 // C:\Users\USERNAME\AppData\Local\Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\runtime
auto localAppDataPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", "");
auto minecraftMSStorePath = auto minecraftMSStorePath =
FS::PathCombine(QFileInfo(partialPath).absolutePath(), "Local", "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe"); FS::PathCombine(QFileInfo(localAppDataPath).absoluteFilePath(), "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe");
minecraftMSStorePath = FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime"); processpaths << FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime");
processpaths << minecraftMSStorePath;
#else #else
partialPath = QDir::homePath(); processpaths << FS::PathCombine(QDir::homePath(), ".minecraft", "runtime");
#endif #endif
auto minecraftDataPath = FS::PathCombine(partialPath, ".minecraft", "runtime");
processpaths << minecraftDataPath;
QStringList javas; QStringList javas;
while (!processpaths.isEmpty()) { while (!processpaths.isEmpty()) {

View File

@ -51,6 +51,7 @@
#include "net/Download.h" #include "net/Download.h"
#include "Application.h" #include "Application.h"
#include "net/NetRequest.h"
namespace { namespace {
QSet<QString> collectPathsFromDir(QString dirPath) QSet<QString> collectPathsFromDir(QString dirPath)
@ -276,7 +277,7 @@ bool reconstructAssets(QString assetsId, QString resourcesFolder)
} // namespace AssetsUtils } // namespace AssetsUtils
NetAction::Ptr AssetObject::getDownloadAction() Net::NetRequest::Ptr AssetObject::getDownloadAction()
{ {
QFileInfo objectFile(getLocalPath()); QFileInfo objectFile(getLocalPath());
if ((!objectFile.isFile()) || (objectFile.size() != size)) { if ((!objectFile.isFile()) || (objectFile.size() != size)) {

View File

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

View File

@ -35,6 +35,7 @@
#include "Library.h" #include "Library.h"
#include "MinecraftInstance.h" #include "MinecraftInstance.h"
#include "net/NetRequest.h"
#include <BuildConfig.h> #include <BuildConfig.h>
#include <FileSystem.h> #include <FileSystem.h>
@ -74,12 +75,12 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
} }
} }
QList<NetAction::Ptr> Library::getDownloads(const RuntimeContext& runtimeContext, QList<Net::NetRequest::Ptr> Library::getDownloads(const RuntimeContext& runtimeContext,
class HttpMetaCache* cache, class HttpMetaCache* cache,
QStringList& failedLocalFiles, QStringList& failedLocalFiles,
const QString& overridePath) const const QString& overridePath) const
{ {
QList<NetAction::Ptr> out; QList<Net::NetRequest::Ptr> out;
bool stale = isAlwaysStale(); bool stale = isAlwaysStale();
bool local = isLocal(); bool local = isLocal();

View File

@ -34,7 +34,6 @@
*/ */
#pragma once #pragma once
#include <net/NetAction.h>
#include <QDir> #include <QDir>
#include <QList> #include <QList>
#include <QMap> #include <QMap>
@ -48,6 +47,7 @@
#include "MojangDownloadInfo.h" #include "MojangDownloadInfo.h"
#include "Rule.h" #include "Rule.h"
#include "RuntimeContext.h" #include "RuntimeContext.h"
#include "net/NetRequest.h"
class Library; class Library;
class MinecraftInstance; class MinecraftInstance;
@ -144,10 +144,10 @@ class Library {
bool isForge() const; bool isForge() const;
// Get a list of downloads for this library // Get a list of downloads for this library
QList<NetAction::Ptr> getDownloads(const RuntimeContext& runtimeContext, QList<Net::NetRequest::Ptr> getDownloads(const RuntimeContext& runtimeContext,
class HttpMetaCache* cache, class HttpMetaCache* cache,
QStringList& failedLocalFiles, QStringList& failedLocalFiles,
const QString& overridePath) const; const QString& overridePath) const;
QString getCompatibleNative(const RuntimeContext& runtimeContext) 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("UseNativeGLFW"), nativeLibraryWorkaroundsOverride);
m_settings->registerOverride(global_settings->getSetting("CustomGLFWPath"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(global_settings->getSetting("CustomGLFWPath"), nativeLibraryWorkaroundsOverride);
// Peformance related options // Performance related options
auto performanceOverride = m_settings->registerSetting("OverridePerformance", false); auto performanceOverride = m_settings->registerSetting("OverridePerformance", false);
m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride);
m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride);
m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride);
m_settings->registerOverride(global_settings->getSetting("UseZink"), performanceOverride);
// Miscellaneous // Miscellaneous
auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false); auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false);
@ -292,10 +293,10 @@ QString MinecraftInstance::gameRoot() const
QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft"));
QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft")); QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft"));
if (mcDir.exists() && !dotMCDir.exists()) if (dotMCDir.exists() && !mcDir.exists())
return mcDir.filePath();
else
return dotMCDir.filePath(); return dotMCDir.filePath();
else
return mcDir.filePath();
} }
QString MinecraftInstance::binRoot() const QString MinecraftInstance::binRoot() const
@ -594,9 +595,6 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
QStringList preloadList; QStringList preloadList;
if (auto value = env.value("LD_PRELOAD"); !value.isEmpty()) if (auto value = env.value("LD_PRELOAD"); !value.isEmpty())
preloadList = value.split(QLatin1String(":")); preloadList = value.split(QLatin1String(":"));
QStringList libPaths;
if (auto value = env.value("LD_LIBRARY_PATH"); !value.isEmpty())
libPaths = value.split(QLatin1String(":"));
auto mangoHudLibString = MangoHud::getLibraryString(); auto mangoHudLibString = MangoHud::getLibraryString();
if (!mangoHudLibString.isEmpty()) { if (!mangoHudLibString.isEmpty()) {
@ -604,18 +602,16 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
QString libPath = mangoHudLib.absolutePath(); QString libPath = mangoHudLib.absolutePath();
auto appendLib = [libPath, &preloadList](QString fileName) { auto appendLib = [libPath, &preloadList](QString fileName) {
if (QFileInfo(FS::PathCombine(libPath, fileName)).exists()) if (QFileInfo(FS::PathCombine(libPath, fileName)).exists())
preloadList << fileName; preloadList << FS::PathCombine(libPath, fileName);
}; };
// dlsym variant is only needed for OpenGL and not included in the vulkan layer // dlsym variant is only needed for OpenGL and not included in the vulkan layer
appendLib("libMangoHud_dlsym.so"); appendLib("libMangoHud_dlsym.so");
appendLib("libMangoHud_opengl.so"); appendLib("libMangoHud_opengl.so");
appendLib(mangoHudLib.fileName()); appendLib(mangoHudLib.fileName());
libPaths << libPath;
} }
env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":"))); env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":")));
env.insert("LD_LIBRARY_PATH", libPaths.join(QLatin1String(":")));
env.insert("MANGOHUD", "1"); env.insert("MANGOHUD", "1");
} }
@ -627,6 +623,13 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only"); env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only");
env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); 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 #endif
return env; return env;
} }
@ -662,8 +665,12 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine
} }
if (serverToJoin && !serverToJoin->address.isEmpty()) { if (serverToJoin && !serverToJoin->address.isEmpty()) {
args_pattern += " --server " + serverToJoin->address; if (profile->hasTrait("feature:is_quick_play_multiplayer")) {
args_pattern += " --port " + QString::number(serverToJoin->port); args_pattern += " --quickPlayMultiplayer " + serverToJoin->address + ':' + QString::number(serverToJoin->port);
} else {
args_pattern += " --server " + serverToJoin->address;
args_pattern += " --port " + QString::number(serverToJoin->port);
}
} }
QMap<QString, QString> token_mapping; QMap<QString, QString> token_mapping;

View File

@ -157,20 +157,6 @@ void MojangVersionFormat::readVersionProperties(const QJsonObject& in, VersionFi
Bits::readString(in, "id", out->minecraftVersion); Bits::readString(in, "id", out->minecraftVersion);
Bits::readString(in, "mainClass", out->mainClass); Bits::readString(in, "mainClass", out->mainClass);
Bits::readString(in, "minecraftArguments", out->minecraftArguments); Bits::readString(in, "minecraftArguments", out->minecraftArguments);
if (out->minecraftArguments.isEmpty()) {
QString processArguments;
Bits::readString(in, "processArguments", processArguments);
QString toCompare = processArguments.toLower();
if (toCompare == "legacy") {
out->minecraftArguments = " ${auth_player_name} ${auth_session}";
} else if (toCompare == "username_session") {
out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session}";
} else if (toCompare == "username_session_version") {
out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session} --version ${profile_name}";
} else if (!toCompare.isEmpty()) {
out->addProblem(ProblemSeverity::Error, QObject::tr("processArguments is set to unknown value '%1'").arg(processArguments));
}
}
Bits::readString(in, "type", out->type); Bits::readString(in, "type", out->type);
Bits::readString(in, "assets", out->assets); Bits::readString(in, "assets", out->assets);

View File

@ -42,7 +42,7 @@
#include <QUuid> #include <QUuid>
namespace { namespace {
void tokenToJSONV3(QJsonObject& parent, Katabasis::Token t, const char* tokenName) void tokenToJSONV3(QJsonObject& parent, Token t, const char* tokenName)
{ {
if (!t.persistent) { if (!t.persistent) {
return; return;
@ -74,9 +74,9 @@ void tokenToJSONV3(QJsonObject& parent, Katabasis::Token t, const char* tokenNam
} }
} }
Katabasis::Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenName) Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenName)
{ {
Katabasis::Token out; Token out;
auto tokenObject = parent.value(tokenName).toObject(); auto tokenObject = parent.value(tokenName).toObject();
if (tokenObject.isEmpty()) { if (tokenObject.isEmpty()) {
return out; return out;
@ -94,7 +94,7 @@ Katabasis::Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenNam
auto token = tokenObject.value("token"); auto token = tokenObject.value("token");
if (token.isString()) { if (token.isString()) {
out.token = token.toString(); out.token = token.toString();
out.validity = Katabasis::Validity::Assumed; out.validity = Validity::Assumed;
} }
auto refresh_token = tokenObject.value("refresh_token"); auto refresh_token = tokenObject.value("refresh_token");
@ -241,13 +241,13 @@ MinecraftProfile profileFromJSONV3(const QJsonObject& parent, const char* tokenN
} }
} }
} }
out.validity = Katabasis::Validity::Assumed; out.validity = Validity::Assumed;
return out; return out;
} }
void entitlementToJSONV3(QJsonObject& parent, MinecraftEntitlement p) void entitlementToJSONV3(QJsonObject& parent, MinecraftEntitlement p)
{ {
if (p.validity == Katabasis::Validity::None) { if (p.validity == Validity::None) {
return; return;
} }
QJsonObject out; QJsonObject out;
@ -271,7 +271,7 @@ bool entitlementFromJSONV3(const QJsonObject& parent, MinecraftEntitlement& out)
} }
out.canPlayMinecraft = canPlayMinecraftV.toBool(false); out.canPlayMinecraft = canPlayMinecraftV.toBool(false);
out.ownsMinecraft = ownsMinecraftV.toBool(false); out.ownsMinecraft = ownsMinecraftV.toBool(false);
out.validity = Katabasis::Validity::Assumed; out.validity = Validity::Assumed;
} }
return true; return true;
} }
@ -313,10 +313,10 @@ bool AccountData::resumeStateFromV3(QJsonObject data)
minecraftProfile = profileFromJSONV3(data, "profile"); minecraftProfile = profileFromJSONV3(data, "profile");
if (!entitlementFromJSONV3(data, minecraftEntitlement)) { if (!entitlementFromJSONV3(data, minecraftEntitlement)) {
if (minecraftProfile.validity != Katabasis::Validity::None) { if (minecraftProfile.validity != Validity::None) {
minecraftEntitlement.canPlayMinecraft = true; minecraftEntitlement.canPlayMinecraft = true;
minecraftEntitlement.ownsMinecraft = true; minecraftEntitlement.ownsMinecraft = true;
minecraftEntitlement.validity = Katabasis::Validity::Assumed; minecraftEntitlement.validity = Validity::Assumed;
} }
} }

View File

@ -34,12 +34,29 @@
*/ */
#pragma once #pragma once
#include <katabasis/Bits.h>
#include <QByteArray> #include <QByteArray>
#include <QJsonObject> #include <QJsonObject>
#include <QString> #include <QString>
#include <QVector> #include <QVector>
#include <QDateTime>
#include <QMap>
#include <QString>
#include <QVariantMap>
enum class Validity { None, Assumed, Certain };
struct Token {
QDateTime issueInstant;
QDateTime notAfter;
QString token;
QString refresh_token;
QVariantMap extra;
Validity validity = Validity::None;
bool persistent = true;
};
struct Skin { struct Skin {
QString id; QString id;
QString url; QString url;
@ -59,7 +76,7 @@ struct Cape {
struct MinecraftEntitlement { struct MinecraftEntitlement {
bool ownsMinecraft = false; bool ownsMinecraft = false;
bool canPlayMinecraft = false; bool canPlayMinecraft = false;
Katabasis::Validity validity = Katabasis::Validity::None; Validity validity = Validity::None;
}; };
struct MinecraftProfile { struct MinecraftProfile {
@ -68,7 +85,7 @@ struct MinecraftProfile {
Skin skin; Skin skin;
QString currentCape; QString currentCape;
QMap<QString, Cape> capes; QMap<QString, Cape> capes;
Katabasis::Validity validity = Katabasis::Validity::None; Validity validity = Validity::None;
}; };
enum class AccountType { MSA, Offline }; enum class AccountType { MSA, Offline };
@ -93,15 +110,15 @@ struct AccountData {
AccountType type = AccountType::MSA; AccountType type = AccountType::MSA;
QString msaClientID; QString msaClientID;
Katabasis::Token msaToken; Token msaToken;
Katabasis::Token userToken; Token userToken;
Katabasis::Token xboxApiToken; Token xboxApiToken;
Katabasis::Token mojangservicesToken; Token mojangservicesToken;
Katabasis::Token yggdrasilToken; Token yggdrasilToken;
MinecraftProfile minecraftProfile; MinecraftProfile minecraftProfile;
MinecraftEntitlement minecraftEntitlement; MinecraftEntitlement minecraftEntitlement;
Katabasis::Validity validity_ = Katabasis::Validity::None; Validity validity_ = Validity::None;
// runtime only information (not saved with the account) // runtime only information (not saved with the account)
QString internalId; QString internalId;

View File

@ -35,7 +35,7 @@
#include "AccountList.h" #include "AccountList.h"
#include "AccountData.h" #include "AccountData.h"
#include "AccountTask.h" #include "tasks/Task.h"
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
@ -52,8 +52,6 @@
#include <FileSystem.h> #include <FileSystem.h>
#include <QSaveFile> #include <QSaveFile>
#include <chrono>
enum AccountListVersion { MojangMSA = 3 }; enum AccountListVersion { MojangMSA = 3 };
AccountList::AccountList(QObject* parent) : QAbstractListModel(parent) AccountList::AccountList(QObject* parent) : QAbstractListModel(parent)
@ -641,8 +639,8 @@ void AccountList::tryNext()
if (account->internalId() == accountId) { if (account->internalId() == accountId) {
m_currentTask = account->refresh(); m_currentTask = account->refresh();
if (m_currentTask) { if (m_currentTask) {
connect(m_currentTask.get(), &AccountTask::succeeded, this, &AccountList::authSucceeded); connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded);
connect(m_currentTask.get(), &AccountTask::failed, this, &AccountList::authFailed); connect(m_currentTask.get(), &Task::failed, this, &AccountList::authFailed);
m_currentTask->start(); m_currentTask->start();
qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID " qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID "
<< accountId; << accountId;

View File

@ -36,6 +36,7 @@
#pragma once #pragma once
#include "MinecraftAccount.h" #include "MinecraftAccount.h"
#include "minecraft/auth/AuthFlow.h"
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QObject> #include <QObject>
@ -144,7 +145,7 @@ class AccountList : public QAbstractListModel {
QList<QString> m_refreshQueue; QList<QString> m_refreshQueue;
QTimer* m_refreshTimer; QTimer* m_refreshTimer;
QTimer* m_nextTimer; QTimer* m_nextTimer;
shared_qobject_ptr<AccountTask> m_currentTask; shared_qobject_ptr<AuthFlow> m_currentTask;
/*! /*!
* Called whenever the list changes. * Called whenever the list changes.

View File

@ -1,134 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "AccountTask.h"
#include "MinecraftAccount.h"
#include <QByteArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QObject>
#include <QString>
#include <QDebug>
AccountTask::AccountTask(AccountData* data, QObject* parent) : Task(parent), m_data(data)
{
changeState(AccountTaskState::STATE_CREATED);
}
QString AccountTask::getStateMessage() const
{
switch (m_taskState) {
case AccountTaskState::STATE_CREATED:
return "Waiting...";
case AccountTaskState::STATE_WORKING:
return tr("Sending request to auth servers...");
case AccountTaskState::STATE_SUCCEEDED:
return tr("Authentication task succeeded.");
case AccountTaskState::STATE_OFFLINE:
return tr("Failed to contact the authentication server.");
case AccountTaskState::STATE_DISABLED:
return tr("Client ID has changed. New session needs to be created.");
case AccountTaskState::STATE_FAILED_SOFT:
return tr("Encountered an error during authentication.");
case AccountTaskState::STATE_FAILED_HARD:
return tr("Failed to authenticate. The session has expired.");
case AccountTaskState::STATE_FAILED_GONE:
return tr("Failed to authenticate. The account no longer exists.");
default:
return tr("...");
}
}
bool AccountTask::changeState(AccountTaskState newState, QString reason)
{
m_taskState = newState;
// FIXME: virtual method invoked in constructor.
// We want that behavior, but maybe make it less weird?
setStatus(getStateMessage());
switch (newState) {
case AccountTaskState::STATE_CREATED: {
m_data->errorString.clear();
return true;
}
case AccountTaskState::STATE_WORKING: {
m_data->accountState = AccountState::Working;
return true;
}
case AccountTaskState::STATE_SUCCEEDED: {
m_data->accountState = AccountState::Online;
emitSucceeded();
return false;
}
case AccountTaskState::STATE_OFFLINE: {
m_data->errorString = reason;
m_data->accountState = AccountState::Offline;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_DISABLED: {
m_data->errorString = reason;
m_data->accountState = AccountState::Disabled;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_SOFT: {
m_data->errorString = reason;
m_data->accountState = AccountState::Errored;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_HARD: {
m_data->errorString = reason;
m_data->accountState = AccountState::Expired;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_GONE: {
m_data->errorString = reason;
m_data->accountState = AccountState::Gone;
emitFailed(reason);
return false;
}
default: {
QString error = tr("Unknown account task state: %1").arg(int(newState));
m_data->accountState = AccountState::Errored;
emitFailed(error);
return false;
}
}
}

View File

@ -1,92 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <tasks/Task.h>
#include <qsslerror.h>
#include <QJsonObject>
#include <QString>
#include <QTimer>
#include "MinecraftAccount.h"
class QNetworkReply;
/**
* Enum for describing the state of the current task.
* Used by the getStateMessage function to determine what the status message should be.
*/
enum class AccountTaskState {
STATE_CREATED,
STATE_WORKING,
STATE_SUCCEEDED,
STATE_DISABLED, //!< MSA Client ID has changed. Tell user to reloginn
STATE_FAILED_SOFT, //!< soft failure. authentication went through partially
STATE_FAILED_HARD, //!< hard failure. main tokens are invalid
STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists
STATE_OFFLINE //!< soft failure. authentication failed in the first step in a 'soft' way
};
class AccountTask : public Task {
Q_OBJECT
public:
explicit AccountTask(AccountData* data, QObject* parent = 0);
virtual ~AccountTask(){};
AccountTaskState m_taskState = AccountTaskState::STATE_CREATED;
AccountTaskState taskState() { return m_taskState; }
signals:
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
void hideVerificationUriAndCode();
protected:
/**
* Returns the state message for the given state.
* Used to set the status message for the task.
* Should be overridden by subclasses that want to change messages for a given state.
*/
virtual QString getStateMessage() const;
protected slots:
// NOTE: true -> non-terminal state, false -> terminal state
bool changeState(AccountTaskState newState, QString reason = QString());
protected:
AccountData* m_data = nullptr;
};

View File

@ -0,0 +1,146 @@
#include <QDebug>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include "minecraft/auth/AccountData.h"
#include "minecraft/auth/steps/EntitlementsStep.h"
#include "minecraft/auth/steps/GetSkinStep.h"
#include "minecraft/auth/steps/LauncherLoginStep.h"
#include "minecraft/auth/steps/MSADeviceCodeStep.h"
#include "minecraft/auth/steps/MSAStep.h"
#include "minecraft/auth/steps/MinecraftProfileStep.h"
#include "minecraft/auth/steps/XboxAuthorizationStep.h"
#include "minecraft/auth/steps/XboxProfileStep.h"
#include "minecraft/auth/steps/XboxUserStep.h"
#include "tasks/Task.h"
#include "AuthFlow.h"
#include <Application.h>
AuthFlow::AuthFlow(AccountData* data, Action action, QObject* parent) : Task(parent), m_data(data)
{
if (data->type == AccountType::MSA) {
if (action == Action::DeviceCode) {
auto oauthStep = makeShared<MSADeviceCodeStep>(m_data);
connect(oauthStep.get(), &MSADeviceCodeStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowserWithExtra);
connect(this, &Task::aborted, oauthStep.get(), &MSADeviceCodeStep::abort);
m_steps.append(oauthStep);
} else {
auto oauthStep = makeShared<MSAStep>(m_data, action == Action::Refresh);
connect(oauthStep.get(), &MSAStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowser);
m_steps.append(oauthStep);
}
m_steps.append(makeShared<XboxUserStep>(m_data));
m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
m_steps.append(
makeShared<XboxAuthorizationStep>(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
m_steps.append(makeShared<LauncherLoginStep>(m_data));
m_steps.append(makeShared<XboxProfileStep>(m_data));
m_steps.append(makeShared<EntitlementsStep>(m_data));
m_steps.append(makeShared<MinecraftProfileStep>(m_data));
m_steps.append(makeShared<GetSkinStep>(m_data));
}
changeState(AccountTaskState::STATE_CREATED);
}
void AuthFlow::succeed()
{
m_data->validity_ = Validity::Certain;
changeState(AccountTaskState::STATE_SUCCEEDED, tr("Finished all authentication steps"));
}
void AuthFlow::executeTask()
{
changeState(AccountTaskState::STATE_WORKING, tr("Initializing"));
nextStep();
}
void AuthFlow::nextStep()
{
if (m_steps.size() == 0) {
// we got to the end without an incident... assume this is all.
m_currentStep.reset();
succeed();
return;
}
m_currentStep = m_steps.front();
qDebug() << "AuthFlow:" << m_currentStep->describe();
m_steps.pop_front();
connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished);
m_currentStep->perform();
}
void AuthFlow::stepFinished(AccountTaskState resultingState, QString message)
{
if (changeState(resultingState, message))
nextStep();
}
bool AuthFlow::changeState(AccountTaskState newState, QString reason)
{
m_taskState = newState;
setDetails(reason);
switch (newState) {
case AccountTaskState::STATE_CREATED: {
setStatus(tr("Waiting..."));
m_data->errorString.clear();
return true;
}
case AccountTaskState::STATE_WORKING: {
setStatus(m_currentStep ? m_currentStep->describe() : tr("Working..."));
m_data->accountState = AccountState::Working;
return true;
}
case AccountTaskState::STATE_SUCCEEDED: {
setStatus(tr("Authentication task succeeded."));
m_data->accountState = AccountState::Online;
emitSucceeded();
return false;
}
case AccountTaskState::STATE_OFFLINE: {
setStatus(tr("Failed to contact the authentication server."));
m_data->errorString = reason;
m_data->accountState = AccountState::Offline;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_DISABLED: {
setStatus(tr("Client ID has changed. New session needs to be created."));
m_data->errorString = reason;
m_data->accountState = AccountState::Disabled;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_SOFT: {
setStatus(tr("Encountered an error during authentication."));
m_data->errorString = reason;
m_data->accountState = AccountState::Errored;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_HARD: {
setStatus(tr("Failed to authenticate. The session has expired."));
m_data->errorString = reason;
m_data->accountState = AccountState::Expired;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_GONE: {
setStatus(tr("Failed to authenticate. The account no longer exists."));
m_data->errorString = reason;
m_data->accountState = AccountState::Gone;
emitFailed(reason);
return false;
}
default: {
setStatus(tr("..."));
QString error = tr("Unknown account task state: %1").arg(int(newState));
m_data->accountState = AccountState::Errored;
emitFailed(error);
return false;
}
}
}

View File

@ -0,0 +1,45 @@
#pragma once
#include <QImage>
#include <QList>
#include <QNetworkReply>
#include <QObject>
#include <QSet>
#include <QVector>
#include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AuthStep.h"
#include "tasks/Task.h"
class AuthFlow : public Task {
Q_OBJECT
public:
enum class Action { Refresh, Login, DeviceCode };
explicit AuthFlow(AccountData* data, Action action = Action::Refresh, QObject* parent = 0);
virtual ~AuthFlow() = default;
void executeTask() override;
AccountTaskState taskState() { return m_taskState; }
signals:
void authorizeWithBrowser(const QUrl& url);
void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn);
protected:
void succeed();
void nextStep();
private slots:
// NOTE: true -> non-terminal state, false -> terminal state
bool changeState(AccountTaskState newState, QString reason = QString());
void stepFinished(AccountTaskState resultingState, QString message);
private:
AccountTaskState m_taskState = AccountTaskState::STATE_CREATED;
QList<AuthStep::Ptr> m_steps;
AuthStep::Ptr m_currentStep;
AccountData* m_data = nullptr;
};

View File

@ -1,175 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <cassert>
#include <QBuffer>
#include <QDebug>
#include <QTimer>
#include <QUrlQuery>
#include "Application.h"
#include "AuthRequest.h"
#include "katabasis/Globals.h"
AuthRequest::AuthRequest(QObject* parent) : QObject(parent) {}
AuthRequest::~AuthRequest() {}
void AuthRequest::get(const QNetworkRequest& req, int timeout /* = 60*1000*/)
{
setup(req, QNetworkAccessManager::GetOperation);
reply_ = APPLICATION->network()->get(request_);
status_ = Requesting;
timedReplies_.add(new Katabasis::Reply(reply_, timeout));
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
connect(reply_, &QNetworkReply::errorOccurred, this, &AuthRequest::onRequestError);
#else // &QNetworkReply::error SIGNAL depricated
connect(reply_, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &AuthRequest::onRequestError);
#endif
connect(reply_, &QNetworkReply::finished, this, &AuthRequest::onRequestFinished);
connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors);
}
void AuthRequest::post(const QNetworkRequest& req, const QByteArray& data, int timeout /* = 60*1000*/)
{
setup(req, QNetworkAccessManager::PostOperation);
data_ = data;
status_ = Requesting;
reply_ = APPLICATION->network()->post(request_, data_);
timedReplies_.add(new Katabasis::Reply(reply_, timeout));
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
connect(reply_, &QNetworkReply::errorOccurred, this, &AuthRequest::onRequestError);
#else // &QNetworkReply::error SIGNAL depricated
connect(reply_, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &AuthRequest::onRequestError);
#endif
connect(reply_, &QNetworkReply::finished, this, &AuthRequest::onRequestFinished);
connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors);
connect(reply_, &QNetworkReply::uploadProgress, this, &AuthRequest::onUploadProgress);
}
void AuthRequest::onRequestFinished()
{
if (status_ == Idle) {
return;
}
if (reply_ != qobject_cast<QNetworkReply*>(sender())) {
return;
}
httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
finish();
}
void AuthRequest::onRequestError(QNetworkReply::NetworkError error)
{
qWarning() << "AuthRequest::onRequestError: Error" << (int)error;
if (status_ == Idle) {
return;
}
if (reply_ != qobject_cast<QNetworkReply*>(sender())) {
return;
}
errorString_ = reply_->errorString();
httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
error_ = error;
qWarning() << "AuthRequest::onRequestError: Error string: " << errorString_;
qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus_
<< reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
// QTimer::singleShot(10, this, SLOT(finish()));
}
void AuthRequest::onSslErrors(QList<QSslError> errors)
{
int i = 1;
for (auto error : errors) {
qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString();
auto cert = error.certificate();
qCritical() << "Certificate in question:\n" << cert.toText();
i++;
}
}
void AuthRequest::onUploadProgress(qint64 uploaded, qint64 total)
{
if (status_ == Idle) {
qWarning() << "AuthRequest::onUploadProgress: No pending request";
return;
}
if (reply_ != qobject_cast<QNetworkReply*>(sender())) {
return;
}
// Restart timeout because request in progress
Katabasis::Reply* o2Reply = timedReplies_.find(reply_);
if (o2Reply) {
o2Reply->start();
}
emit uploadProgress(uploaded, total);
}
void AuthRequest::setup(const QNetworkRequest& req, QNetworkAccessManager::Operation operation, const QByteArray& verb)
{
request_ = req;
operation_ = operation;
url_ = req.url();
QUrl url = url_;
request_.setUrl(url);
if (!verb.isEmpty()) {
request_.setRawHeader(Katabasis::HTTP_HTTP_HEADER, verb);
}
status_ = Requesting;
error_ = QNetworkReply::NoError;
errorString_.clear();
httpStatus_ = 0;
}
void AuthRequest::finish()
{
QByteArray data;
if (status_ == Idle) {
qWarning() << "AuthRequest::finish: No pending request";
return;
}
data = reply_->readAll();
status_ = Idle;
timedReplies_.remove(reply_);
reply_->disconnect(this);
reply_->deleteLater();
QList<QNetworkReply::RawHeaderPair> headers = reply_->rawHeaderPairs();
emit finished(error_, data, headers);
}

View File

@ -1,67 +0,0 @@
#pragma once
#include <QByteArray>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QObject>
#include <QUrl>
#include "katabasis/Reply.h"
/// Makes authentication requests.
class AuthRequest : public QObject {
Q_OBJECT
public:
explicit AuthRequest(QObject* parent = 0);
~AuthRequest();
public slots:
void get(const QNetworkRequest& req, int timeout = 60 * 1000);
void post(const QNetworkRequest& req, const QByteArray& data, int timeout = 60 * 1000);
signals:
/// Emitted when a request has been completed or failed.
void finished(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers);
/// Emitted when an upload has progressed.
void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
protected slots:
/// Handle request finished.
void onRequestFinished();
/// Handle request error.
void onRequestError(QNetworkReply::NetworkError error);
/// Handle ssl errors.
void onSslErrors(QList<QSslError> errors);
/// Finish the request, emit finished() signal.
void finish();
/// Handle upload progress.
void onUploadProgress(qint64 uploaded, qint64 total);
public:
QNetworkReply::NetworkError error_;
int httpStatus_ = 0;
QString errorString_;
protected:
void setup(const QNetworkRequest& request, QNetworkAccessManager::Operation operation, const QByteArray& verb = QByteArray());
enum Status { Idle, Requesting, ReRequesting };
QNetworkRequest request_;
QByteArray data_;
QNetworkReply* reply_;
Status status_;
QNetworkAccessManager::Operation operation_;
QUrl url_;
Katabasis::ReplyList timedReplies_;
QTimer* timer_;
};

View File

@ -1,5 +0,0 @@
#include "AuthStep.h"
AuthStep::AuthStep(AccountData* data) : QObject(nullptr), m_data(data) {}
AuthStep::~AuthStep() noexcept = default;

View File

@ -3,30 +3,40 @@
#include <QNetworkReply> #include <QNetworkReply>
#include <QObject> #include <QObject>
#include "AccountTask.h"
#include "QObjectPtr.h" #include "QObjectPtr.h"
#include "minecraft/auth/AccountData.h" #include "minecraft/auth/AccountData.h"
/**
* Enum for describing the state of the current task.
* Used by the getStateMessage function to determine what the status message should be.
*/
enum class AccountTaskState {
STATE_CREATED,
STATE_WORKING,
STATE_SUCCEEDED,
STATE_DISABLED, //!< MSA Client ID has changed. Tell user to reloginn
STATE_FAILED_SOFT, //!< soft failure. authentication went through partially
STATE_FAILED_HARD, //!< hard failure. main tokens are invalid
STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists
STATE_OFFLINE //!< soft failure. authentication failed in the first step in a 'soft' way
};
class AuthStep : public QObject { class AuthStep : public QObject {
Q_OBJECT Q_OBJECT
public: public:
using Ptr = shared_qobject_ptr<AuthStep>; using Ptr = shared_qobject_ptr<AuthStep>;
public: explicit AuthStep(AccountData* data) : QObject(nullptr), m_data(data){};
explicit AuthStep(AccountData* data); virtual ~AuthStep() noexcept = default;
virtual ~AuthStep() noexcept;
virtual QString describe() = 0; virtual QString describe() = 0;
public slots: public slots:
virtual void perform() = 0; virtual void perform() = 0;
virtual void rehydrate() = 0;
signals: signals:
void finished(AccountTaskState resultingState, QString message); void finished(AccountTaskState resultingState, QString message);
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
void hideVerificationUriAndCode();
protected: protected:
AccountData* m_data; AccountData* m_data;

View File

@ -50,9 +50,8 @@
#include <QPainter> #include <QPainter>
#include "flows/MSA.h"
#include "flows/Offline.h"
#include "minecraft/auth/AccountData.h" #include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AuthFlow.h"
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent)
{ {
@ -80,7 +79,7 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username)
auto account = makeShared<MinecraftAccount>(); auto account = makeShared<MinecraftAccount>();
account->data.type = AccountType::Offline; account->data.type = AccountType::Offline;
account->data.yggdrasilToken.token = "0"; account->data.yggdrasilToken.token = "0";
account->data.yggdrasilToken.validity = Katabasis::Validity::Certain; account->data.yggdrasilToken.validity = Validity::Certain;
account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
account->data.yggdrasilToken.extra["userName"] = username; account->data.yggdrasilToken.extra["userName"] = username;
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
@ -88,7 +87,7 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username)
account->data.minecraftEntitlement.canPlayMinecraft = true; account->data.minecraftEntitlement.canPlayMinecraft = true;
account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]")); account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftProfile.name = username; account->data.minecraftProfile.name = username;
account->data.minecraftProfile.validity = Katabasis::Validity::Certain; account->data.minecraftProfile.validity = Validity::Certain;
return account; return account;
} }
@ -120,11 +119,11 @@ QPixmap MinecraftAccount::getFace() const
return skin.scaled(64, 64, Qt::KeepAspectRatio); return skin.scaled(64, 64, Qt::KeepAspectRatio);
} }
shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA() shared_qobject_ptr<AuthFlow> MinecraftAccount::login(bool useDeviceCode)
{ {
Q_ASSERT(m_currentTask.get() == nullptr); Q_ASSERT(m_currentTask.get() == nullptr);
m_currentTask.reset(new MSAInteractive(&data)); m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login, this));
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); }); connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); });
@ -132,29 +131,13 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA()
return m_currentTask; return m_currentTask;
} }
shared_qobject_ptr<AccountTask> MinecraftAccount::loginOffline() shared_qobject_ptr<AuthFlow> MinecraftAccount::refresh()
{
Q_ASSERT(m_currentTask.get() == nullptr);
m_currentTask.reset(new OfflineLogin(&data));
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); });
emit activityChanged(true);
return m_currentTask;
}
shared_qobject_ptr<AccountTask> MinecraftAccount::refresh()
{ {
if (m_currentTask) { if (m_currentTask) {
return m_currentTask; return m_currentTask;
} }
if (data.type == AccountType::MSA) { m_currentTask.reset(new AuthFlow(&data, AuthFlow::Action::Refresh, this));
m_currentTask.reset(new MSASilent(&data));
} else {
m_currentTask.reset(new OfflineRefresh(&data));
}
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
@ -163,7 +146,7 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::refresh()
return m_currentTask; return m_currentTask;
} }
shared_qobject_ptr<AccountTask> MinecraftAccount::currentTask() shared_qobject_ptr<AuthFlow> MinecraftAccount::currentTask()
{ {
return m_currentTask; return m_currentTask;
} }
@ -189,17 +172,17 @@ void MinecraftAccount::authFailed(QString reason)
if (accountType() == AccountType::MSA) { if (accountType() == AccountType::MSA) {
data.msaToken.token = QString(); data.msaToken.token = QString();
data.msaToken.refresh_token = QString(); data.msaToken.refresh_token = QString();
data.msaToken.validity = Katabasis::Validity::None; data.msaToken.validity = Validity::None;
data.validity_ = Katabasis::Validity::None; data.validity_ = Validity::None;
} else { } else {
data.yggdrasilToken.token = QString(); data.yggdrasilToken.token = QString();
data.yggdrasilToken.validity = Katabasis::Validity::None; data.yggdrasilToken.validity = Validity::None;
data.validity_ = Katabasis::Validity::None; data.validity_ = Validity::None;
} }
emit changed(); emit changed();
} break; } break;
case AccountTaskState::STATE_FAILED_GONE: { case AccountTaskState::STATE_FAILED_GONE: {
data.validity_ = Katabasis::Validity::None; data.validity_ = Validity::None;
emit changed(); emit changed();
} break; } break;
case AccountTaskState::STATE_CREATED: case AccountTaskState::STATE_CREATED:
@ -229,13 +212,13 @@ bool MinecraftAccount::shouldRefresh() const
return false; return false;
} }
switch (data.validity_) { switch (data.validity_) {
case Katabasis::Validity::Certain: { case Validity::Certain: {
break; break;
} }
case Katabasis::Validity::None: { case Validity::None: {
return false; return false;
} }
case Katabasis::Validity::Assumed: { case Validity::Assumed: {
return true; return true;
} }
} }

View File

@ -43,15 +43,13 @@
#include <QPixmap> #include <QPixmap>
#include <QString> #include <QString>
#include <memory>
#include "AccountData.h" #include "AccountData.h"
#include "AuthSession.h" #include "AuthSession.h"
#include "QObjectPtr.h" #include "QObjectPtr.h"
#include "Usable.h" #include "Usable.h"
#include "minecraft/auth/AuthFlow.h"
class Task; class Task;
class AccountTask;
class MinecraftAccount; class MinecraftAccount;
using MinecraftAccountPtr = shared_qobject_ptr<MinecraftAccount>; using MinecraftAccountPtr = shared_qobject_ptr<MinecraftAccount>;
@ -97,13 +95,11 @@ class MinecraftAccount : public QObject, public Usable {
QJsonObject saveToJson() const; QJsonObject saveToJson() const;
public: /* manipulation */ public: /* manipulation */
shared_qobject_ptr<AccountTask> loginMSA(); shared_qobject_ptr<AuthFlow> login(bool useDeviceCode = false);
shared_qobject_ptr<AccountTask> loginOffline(); shared_qobject_ptr<AuthFlow> refresh();
shared_qobject_ptr<AccountTask> refresh(); shared_qobject_ptr<AuthFlow> currentTask();
shared_qobject_ptr<AccountTask> currentTask();
public: /* queries */ public: /* queries */
QString internalId() const { return data.internalId; } QString internalId() const { return data.internalId; }
@ -166,7 +162,7 @@ class MinecraftAccount : public QObject, public Usable {
AccountData data; AccountData data;
// current task we are executing here // current task we are executing here
shared_qobject_ptr<AccountTask> m_currentTask; shared_qobject_ptr<AuthFlow> m_currentTask;
protected: /* methods */ protected: /* methods */
void incrementUses() override; void incrementUses() override;

View File

@ -79,7 +79,7 @@ bool getBool(QJsonValue value, bool& out)
// 2148916238 = child account not linked to a family // 2148916238 = child account not linked to a family
*/ */
bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString name) bool parseXTokenResponse(QByteArray& data, Token& output, QString name)
{ {
qDebug() << "Parsing" << name << ":"; qDebug() << "Parsing" << name << ":";
qCDebug(authCredentials()) << data; qCDebug(authCredentials()) << data;
@ -135,7 +135,7 @@ bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString nam
qWarning() << "Missing uhs"; qWarning() << "Missing uhs";
return false; return false;
} }
output.validity = Katabasis::Validity::Certain; output.validity = Validity::Certain;
qDebug() << name << "is valid."; qDebug() << name << "is valid.";
return true; return true;
} }
@ -213,7 +213,7 @@ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output)
output.capes[capeOut.id] = capeOut; output.capes[capeOut.id] = capeOut;
} }
output.currentCape = currentCape; output.currentCape = currentCape;
output.validity = Katabasis::Validity::Certain; output.validity = Validity::Certain;
return true; return true;
} }
@ -388,7 +388,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output)
output.currentCape = capeOut.alias; output.currentCape = capeOut.alias;
} }
output.validity = Katabasis::Validity::Certain; output.validity = Validity::Certain;
return true; return true;
} }
@ -422,7 +422,7 @@ bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output)
output.ownsMinecraft = true; output.ownsMinecraft = true;
} }
} }
output.validity = Katabasis::Validity::Certain; output.validity = Validity::Certain;
return true; return true;
} }
@ -456,7 +456,7 @@ bool parseRolloutResponse(QByteArray& data, bool& result)
return true; return true;
} }
bool parseMojangResponse(QByteArray& data, Katabasis::Token& output) bool parseMojangResponse(QByteArray& data, Token& output)
{ {
QJsonParseError jsonError; QJsonParseError jsonError;
qDebug() << "Parsing Mojang response..."; qDebug() << "Parsing Mojang response...";
@ -488,7 +488,7 @@ bool parseMojangResponse(QByteArray& data, Katabasis::Token& output)
qWarning() << "access_token is not valid"; qWarning() << "access_token is not valid";
return false; return false;
} }
output.validity = Katabasis::Validity::Certain; output.validity = Validity::Certain;
qDebug() << "Mojang response is valid."; qDebug() << "Mojang response is valid.";
return true; return true;
} }

View File

@ -9,8 +9,8 @@ bool getNumber(QJsonValue value, double& out);
bool getNumber(QJsonValue value, int64_t& out); bool getNumber(QJsonValue value, int64_t& out);
bool getBool(QJsonValue value, bool& out); bool getBool(QJsonValue value, bool& out);
bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString name); bool parseXTokenResponse(QByteArray& data, Token& output, QString name);
bool parseMojangResponse(QByteArray& data, Katabasis::Token& output); bool parseMojangResponse(QByteArray& data, Token& output);
bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output); bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output);
bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output); bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output);

View File

@ -1,67 +0,0 @@
#include <QDebug>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include "AuthFlow.h"
#include "katabasis/Globals.h"
#include <Application.h>
AuthFlow::AuthFlow(AccountData* data, QObject* parent) : AccountTask(data, parent) {}
void AuthFlow::succeed()
{
m_data->validity_ = Katabasis::Validity::Certain;
changeState(AccountTaskState::STATE_SUCCEEDED, tr("Finished all authentication steps"));
}
void AuthFlow::executeTask()
{
if (m_currentStep) {
return;
}
changeState(AccountTaskState::STATE_WORKING, tr("Initializing"));
nextStep();
}
void AuthFlow::nextStep()
{
if (m_steps.size() == 0) {
// we got to the end without an incident... assume this is all.
m_currentStep.reset();
succeed();
return;
}
m_currentStep = m_steps.front();
qDebug() << "AuthFlow:" << m_currentStep->describe();
m_steps.pop_front();
connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished);
connect(m_currentStep.get(), &AuthStep::showVerificationUriAndCode, this, &AuthFlow::showVerificationUriAndCode);
connect(m_currentStep.get(), &AuthStep::hideVerificationUriAndCode, this, &AuthFlow::hideVerificationUriAndCode);
m_currentStep->perform();
}
QString AuthFlow::getStateMessage() const
{
switch (m_taskState) {
case AccountTaskState::STATE_WORKING: {
if (m_currentStep) {
return m_currentStep->describe();
} else {
return tr("Working...");
}
}
default: {
return AccountTask::getStateMessage();
}
}
}
void AuthFlow::stepFinished(AccountTaskState resultingState, QString message)
{
if (changeState(resultingState, message)) {
nextStep();
}
}

View File

@ -1,41 +0,0 @@
#pragma once
#include <QImage>
#include <QList>
#include <QNetworkReply>
#include <QObject>
#include <QSet>
#include <QVector>
#include <katabasis/DeviceFlow.h>
#include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AccountTask.h"
#include "minecraft/auth/AuthStep.h"
class AuthFlow : public AccountTask {
Q_OBJECT
public:
explicit AuthFlow(AccountData* data, QObject* parent = 0);
Katabasis::Validity validity() { return m_data->validity_; };
QString getStateMessage() const override;
void executeTask() override;
signals:
void activityChanged(Katabasis::Activity activity);
private slots:
void stepFinished(AccountTaskState resultingState, QString message);
protected:
void succeed();
void nextStep();
protected:
QList<AuthStep::Ptr> m_steps;
AuthStep::Ptr m_currentStep;
};

View File

@ -1,36 +0,0 @@
#include "MSA.h"
#include "minecraft/auth/steps/EntitlementsStep.h"
#include "minecraft/auth/steps/GetSkinStep.h"
#include "minecraft/auth/steps/LauncherLoginStep.h"
#include "minecraft/auth/steps/MSAStep.h"
#include "minecraft/auth/steps/MinecraftProfileStep.h"
#include "minecraft/auth/steps/XboxAuthorizationStep.h"
#include "minecraft/auth/steps/XboxProfileStep.h"
#include "minecraft/auth/steps/XboxUserStep.h"
MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthFlow(data, parent)
{
m_steps.append(makeShared<MSAStep>(m_data, MSAStep::Action::Refresh));
m_steps.append(makeShared<XboxUserStep>(m_data));
m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
m_steps.append(makeShared<LauncherLoginStep>(m_data));
m_steps.append(makeShared<XboxProfileStep>(m_data));
m_steps.append(makeShared<EntitlementsStep>(m_data));
m_steps.append(makeShared<MinecraftProfileStep>(m_data));
m_steps.append(makeShared<GetSkinStep>(m_data));
}
MSAInteractive::MSAInteractive(AccountData* data, QObject* parent) : AuthFlow(data, parent)
{
m_steps.append(makeShared<MSAStep>(m_data, MSAStep::Action::Login));
m_steps.append(makeShared<XboxUserStep>(m_data));
m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
m_steps.append(makeShared<LauncherLoginStep>(m_data));
m_steps.append(makeShared<XboxProfileStep>(m_data));
m_steps.append(makeShared<EntitlementsStep>(m_data));
m_steps.append(makeShared<MinecraftProfileStep>(m_data));
m_steps.append(makeShared<GetSkinStep>(m_data));
}

View File

@ -1,14 +0,0 @@
#pragma once
#include "AuthFlow.h"
class MSAInteractive : public AuthFlow {
Q_OBJECT
public:
explicit MSAInteractive(AccountData* data, QObject* parent = 0);
};
class MSASilent : public AuthFlow {
Q_OBJECT
public:
explicit MSASilent(AccountData* data, QObject* parent = 0);
};

View File

@ -1,13 +0,0 @@
#include "Offline.h"
#include "minecraft/auth/steps/OfflineStep.h"
OfflineRefresh::OfflineRefresh(AccountData* data, QObject* parent) : AuthFlow(data, parent)
{
m_steps.append(makeShared<OfflineStep>(m_data));
}
OfflineLogin::OfflineLogin(AccountData* data, QObject* parent) : AuthFlow(data, parent)
{
m_steps.append(makeShared<OfflineStep>(m_data));
}

View File

@ -1,14 +0,0 @@
#pragma once
#include "AuthFlow.h"
class OfflineRefresh : public AuthFlow {
Q_OBJECT
public:
explicit OfflineRefresh(AccountData* data, QObject* parent = 0);
};
class OfflineLogin : public AuthFlow {
Q_OBJECT
public:
explicit OfflineLogin(AccountData* data, QObject* parent = 0);
};

View File

@ -1,16 +1,20 @@
#include "EntitlementsStep.h" #include "EntitlementsStep.h"
#include <QList>
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QUrl>
#include <QUuid> #include <QUuid>
#include <memory>
#include "Application.h"
#include "Logging.h" #include "Logging.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
#include "net/Download.h"
#include "net/StaticHeaderProxy.h"
#include "tasks/Task.h"
EntitlementsStep::EntitlementsStep(AccountData* data) : AuthStep(data) {} EntitlementsStep::EntitlementsStep(AccountData* data) : AuthStep(data) {}
EntitlementsStep::~EntitlementsStep() noexcept = default;
QString EntitlementsStep::describe() QString EntitlementsStep::describe()
{ {
return tr("Determining game ownership."); return tr("Determining game ownership.");
@ -19,35 +23,31 @@ QString EntitlementsStep::describe()
void EntitlementsStep::perform() void EntitlementsStep::perform()
{ {
auto uuid = QUuid::createUuid(); auto uuid = QUuid::createUuid();
m_entitlementsRequestId = uuid.toString().remove('{').remove('}'); m_entitlements_request_id = uuid.toString().remove('{').remove('}');
auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlementsRequestId;
QNetworkRequest request = QNetworkRequest(url); QUrl url("https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlements_request_id);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" },
request.setRawHeader("Accept", "application/json"); { "Accept", "application/json" },
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } };
AuthRequest* requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &EntitlementsStep::onRequestDone); m_response.reset(new QByteArray());
requestor->get(request); m_task = Net::Download::makeByteArray(url, m_response);
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_task.get(), &Task::finished, this, &EntitlementsStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start();
qDebug() << "Getting entitlements..."; qDebug() << "Getting entitlements...";
} }
void EntitlementsStep::rehydrate() void EntitlementsStep::onRequestDone()
{ {
// NOOP, for now. We only save bools and there's nothing to check. qCDebug(authCredentials()) << *m_response;
}
void EntitlementsStep::onRequestDone([[maybe_unused]] QNetworkReply::NetworkError error,
QByteArray data,
[[maybe_unused]] QList<QNetworkReply::RawHeaderPair> headers)
{
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
requestor->deleteLater();
qCDebug(authCredentials()) << data;
// TODO: check presence of same entitlementsRequestId? // TODO: check presence of same entitlementsRequestId?
// TODO: validate JWTs? // TODO: validate JWTs?
Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement); Parsers::parseMinecraftEntitlements(*m_response, m_data->minecraftEntitlement);
emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements")); emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements"));
} }

View File

@ -1,24 +1,26 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <memory>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/Download.h"
class EntitlementsStep : public AuthStep { class EntitlementsStep : public AuthStep {
Q_OBJECT Q_OBJECT
public: public:
explicit EntitlementsStep(AccountData* data); explicit EntitlementsStep(AccountData* data);
virtual ~EntitlementsStep() noexcept; virtual ~EntitlementsStep() noexcept = default;
void perform() override; void perform() override;
void rehydrate() override;
QString describe() override; QString describe() override;
private slots: private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); void onRequestDone();
private: private:
QString m_entitlementsRequestId; QString m_entitlements_request_id;
std::shared_ptr<QByteArray> m_response;
Net::Download::Ptr m_task;
}; };

View File

@ -3,13 +3,10 @@
#include <QNetworkRequest> #include <QNetworkRequest>
#include "minecraft/auth/AuthRequest.h" #include "Application.h"
#include "minecraft/auth/Parsers.h"
GetSkinStep::GetSkinStep(AccountData* data) : AuthStep(data) {} GetSkinStep::GetSkinStep(AccountData* data) : AuthStep(data) {}
GetSkinStep::~GetSkinStep() noexcept = default;
QString GetSkinStep::describe() QString GetSkinStep::describe()
{ {
return tr("Getting skin."); return tr("Getting skin.");
@ -17,25 +14,20 @@ QString GetSkinStep::describe()
void GetSkinStep::perform() void GetSkinStep::perform()
{ {
auto url = QUrl(m_data->minecraftProfile.skin.url); QUrl url(m_data->minecraftProfile.skin.url);
QNetworkRequest request = QNetworkRequest(url);
AuthRequest* requestor = new AuthRequest(this); m_response.reset(new QByteArray());
connect(requestor, &AuthRequest::finished, this, &GetSkinStep::onRequestDone); m_task = Net::Download::makeByteArray(url, m_response);
requestor->get(request);
connect(m_task.get(), &Task::finished, this, &GetSkinStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start();
} }
void GetSkinStep::rehydrate() void GetSkinStep::onRequestDone()
{ {
// NOOP, for now. if (m_task->error() == QNetworkReply::NoError)
} m_data->minecraftProfile.skin.data = *m_response;
void GetSkinStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers)
{
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
requestor->deleteLater();
if (error == QNetworkReply::NoError) {
m_data->minecraftProfile.skin.data = data;
}
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin")); emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin"));
} }

View File

@ -1,21 +1,25 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <memory>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/Download.h"
class GetSkinStep : public AuthStep { class GetSkinStep : public AuthStep {
Q_OBJECT Q_OBJECT
public: public:
explicit GetSkinStep(AccountData* data); explicit GetSkinStep(AccountData* data);
virtual ~GetSkinStep() noexcept; virtual ~GetSkinStep() noexcept = default;
void perform() override; void perform() override;
void rehydrate() override;
QString describe() override; QString describe() override;
private slots: private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); void onRequestDone();
private:
std::shared_ptr<QByteArray> m_response;
Net::Download::Ptr m_task;
}; };

View File

@ -1,17 +1,17 @@
#include "LauncherLoginStep.h" #include "LauncherLoginStep.h"
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QUrl>
#include "Application.h"
#include "Logging.h" #include "Logging.h"
#include "minecraft/auth/AccountTask.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h" #include "net/NetUtils.h"
#include "net/StaticHeaderProxy.h"
#include "net/Upload.h"
LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {} LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {}
LauncherLoginStep::~LauncherLoginStep() noexcept = default;
QString LauncherLoginStep::describe() QString LauncherLoginStep::describe()
{ {
return tr("Accessing Mojang services."); return tr("Accessing Mojang services.");
@ -19,7 +19,7 @@ QString LauncherLoginStep::describe()
void LauncherLoginStep::perform() void LauncherLoginStep::perform()
{ {
auto requestURL = "https://api.minecraftservices.com/launcher/login"; QUrl url("https://api.minecraftservices.com/launcher/login");
auto uhs = m_data->mojangservicesToken.extra["uhs"].toString(); auto uhs = m_data->mojangservicesToken.extra["uhs"].toString();
auto xToken = m_data->mojangservicesToken.token; auto xToken = m_data->mojangservicesToken.token;
@ -31,40 +31,37 @@ void LauncherLoginStep::perform()
)XXX"; )XXX";
auto requestBody = mc_auth_template.arg(uhs, xToken); auto requestBody = mc_auth_template.arg(uhs, xToken);
QNetworkRequest request = QNetworkRequest(QUrl(requestURL)); auto headers = QList<Net::HeaderPair>{
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); { "Content-Type", "application/json" },
request.setRawHeader("Accept", "application/json"); { "Accept", "application/json" },
AuthRequest* requestor = new AuthRequest(this); };
connect(requestor, &AuthRequest::finished, this, &LauncherLoginStep::onRequestDone);
requestor->post(request, requestBody.toUtf8()); m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8());
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_task.get(), &Task::finished, this, &LauncherLoginStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start();
qDebug() << "Getting Minecraft access token..."; qDebug() << "Getting Minecraft access token...";
} }
void LauncherLoginStep::rehydrate() void LauncherLoginStep::onRequestDone()
{ {
// TODO: check the token validity qCDebug(authCredentials()) << *m_response;
} if (m_task->error() != QNetworkReply::NoError) {
qWarning() << "Reply error:" << m_task->error();
void LauncherLoginStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) if (Net::isApplicationError(m_task->error())) {
{ emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get Minecraft access token: %1").arg(m_task->errorString()));
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
requestor->deleteLater();
qCDebug(authCredentials()) << data;
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
qCDebug(authCredentials()) << data;
if (Net::isApplicationError(error)) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_));
} else { } else {
emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_)); emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(m_task->errorString()));
} }
return; return;
} }
if (!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) { if (!Parsers::parseMojangResponse(*m_response, m_data->yggdrasilToken)) {
qWarning() << "Could not parse login_with_xbox response..."; qWarning() << "Could not parse login_with_xbox response...";
qCDebug(authCredentials()) << data;
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to parse the Minecraft access token response.")); emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to parse the Minecraft access token response."));
return; return;
} }

View File

@ -1,21 +1,25 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <memory>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/Upload.h"
class LauncherLoginStep : public AuthStep { class LauncherLoginStep : public AuthStep {
Q_OBJECT Q_OBJECT
public: public:
explicit LauncherLoginStep(AccountData* data); explicit LauncherLoginStep(AccountData* data);
virtual ~LauncherLoginStep() noexcept; virtual ~LauncherLoginStep() noexcept = default;
void perform() override; void perform() override;
void rehydrate() override;
QString describe() override; QString describe() override;
private slots: private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); void onRequestDone();
private:
std::shared_ptr<QByteArray> m_response;
Net::Upload::Ptr m_task;
}; };

View File

@ -0,0 +1,270 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "MSADeviceCodeStep.h"
#include <QDateTime>
#include <QUrlQuery>
#include "Application.h"
#include "Json.h"
#include "net/StaticHeaderProxy.h"
// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code
MSADeviceCodeStep::MSADeviceCodeStep(AccountData* data) : AuthStep(data)
{
m_clientId = APPLICATION->getMSAClientID();
}
QString MSADeviceCodeStep::describe()
{
return tr("Logging in with Microsoft account(device code).");
}
void MSADeviceCodeStep::perform()
{
QUrlQuery data;
data.addQueryItem("client_id", m_clientId);
data.addQueryItem("scope", "XboxLive.SignIn XboxLive.offline_access");
auto payload = data.query(QUrl::FullyEncoded).toUtf8();
QUrl url("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode");
auto headers = QList<Net::HeaderPair>{
{ "Content-Type", "application/x-www-form-urlencoded" },
{ "Accept", "application/json" },
};
m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, payload);
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAutorizationFinished);
m_task->setNetwork(APPLICATION->network());
m_task->start();
}
struct DeviceAutorizationResponse {
QString device_code;
QString user_code;
QString verification_uri;
int expires_in;
int interval;
QString error;
QString error_description;
};
DeviceAutorizationResponse parseDeviceAutorizationResponse(const QByteArray& data)
{
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
if (err.error != QJsonParseError::NoError) {
qWarning() << "Failed to parse device autorization response due to err:" << err.errorString();
return {};
}
if (!doc.isObject()) {
qWarning() << "Device autorization response is not an object";
return {};
}
auto obj = doc.object();
return {
Json::ensureString(obj, "device_code"), Json::ensureString(obj, "user_code"), Json::ensureString(obj, "verification_uri"),
Json::ensureInteger(obj, "expires_in"), Json::ensureInteger(obj, "interval"), Json::ensureString(obj, "error"),
Json::ensureString(obj, "error_description"),
};
}
void MSADeviceCodeStep::deviceAutorizationFinished()
{
auto rsp = parseDeviceAutorizationResponse(*m_response);
if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) {
qWarning() << "Device authorization failed:" << rsp.error;
emit finished(AccountTaskState::STATE_FAILED_HARD,
tr("Device authorization failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description));
return;
}
if (!m_task->wasSuccessful() || m_task->error() != QNetworkReply::NoError) {
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Failed to retrieve device authorization"));
qDebug() << *m_response;
return;
}
if (rsp.device_code.isEmpty() || rsp.user_code.isEmpty() || rsp.verification_uri.isEmpty() || rsp.expires_in == 0) {
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: required fields missing"));
return;
}
if (rsp.interval != 0) {
interval = rsp.interval;
}
m_device_code = rsp.device_code;
emit authorizeWithBrowser(rsp.verification_uri, rsp.user_code, rsp.expires_in);
m_expiration_timer.setTimerType(Qt::VeryCoarseTimer);
m_expiration_timer.setInterval(rsp.expires_in * 1000);
m_expiration_timer.setSingleShot(true);
connect(&m_expiration_timer, &QTimer::timeout, this, &MSADeviceCodeStep::abort);
m_expiration_timer.start();
m_pool_timer.setTimerType(Qt::VeryCoarseTimer);
m_pool_timer.setSingleShot(true);
startPoolTimer();
}
void MSADeviceCodeStep::abort()
{
m_expiration_timer.stop();
m_pool_timer.stop();
if (m_task) {
m_task->abort();
}
m_is_aborted = true;
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Task aborted"));
}
void MSADeviceCodeStep::startPoolTimer()
{
if (m_is_aborted) {
return;
}
m_pool_timer.setInterval(interval * 1000);
connect(&m_pool_timer, &QTimer::timeout, this, &MSADeviceCodeStep::authenticateUser);
m_pool_timer.start();
}
void MSADeviceCodeStep::authenticateUser()
{
QUrlQuery data;
data.addQueryItem("client_id", m_clientId);
data.addQueryItem("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
data.addQueryItem("device_code", m_device_code);
auto payload = data.query(QUrl::FullyEncoded).toUtf8();
QUrl url("https://login.microsoftonline.com/consumers/oauth2/v2.0/token");
auto headers = QList<Net::HeaderPair>{
{ "Content-Type", "application/x-www-form-urlencoded" },
{ "Accept", "application/json" },
};
m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, payload);
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::authenticationFinished);
m_task->setNetwork(APPLICATION->network());
m_task->start();
}
struct AuthenticationResponse {
QString access_token;
QString token_type;
QString refresh_token;
int expires_in;
QString error;
QString error_description;
QVariantMap extra;
};
AuthenticationResponse parseAuthenticationResponse(const QByteArray& data)
{
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
if (err.error != QJsonParseError::NoError) {
qWarning() << "Failed to parse device autorization response due to err:" << err.errorString();
return {};
}
if (!doc.isObject()) {
qWarning() << "Device autorization response is not an object";
return {};
}
auto obj = doc.object();
return { Json::ensureString(obj, "access_token"),
Json::ensureString(obj, "token_type"),
Json::ensureString(obj, "refresh_token"),
Json::ensureInteger(obj, "expires_in"),
Json::ensureString(obj, "error"),
Json::ensureString(obj, "error_description"),
obj.toVariantMap() };
}
void MSADeviceCodeStep::authenticationFinished()
{
if (m_task->error() == QNetworkReply::TimeoutError) {
// rfc8628#section-3.5
// "On encountering a connection timeout, clients MUST unilaterally
// reduce their polling frequency before retrying. The use of an
// exponential backoff algorithm to achieve this, such as doubling the
// polling interval on each such connection timeout, is RECOMMENDED."
interval *= 2;
startPoolTimer();
return;
}
auto rsp = parseAuthenticationResponse(*m_response);
if (rsp.error == "slow_down") {
// rfc8628#section-3.5
// "A variant of 'authorization_pending', the authorization request is
// still pending and polling should continue, but the interval MUST
// be increased by 5 seconds for this and all subsequent requests."
interval += 5;
startPoolTimer();
return;
}
if (rsp.error == "authorization_pending") {
// keep trying - rfc8628#section-3.5
// "The authorization request is still pending as the end user hasn't
// yet completed the user-interaction steps (Section 3.3)."
startPoolTimer();
return;
}
if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) {
qWarning() << "Device Access failed:" << rsp.error;
emit finished(AccountTaskState::STATE_FAILED_HARD,
tr("Device Access failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description));
return;
}
if (!m_task->wasSuccessful() || m_task->error() != QNetworkReply::NoError) {
startPoolTimer(); // it failed so just try again without increasing the interval
return;
}
m_expiration_timer.stop();
m_data->msaClientID = m_clientId;
m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc();
m_data->msaToken.notAfter = QDateTime::currentDateTime().addSecs(rsp.expires_in);
m_data->msaToken.extra = rsp.extra;
m_data->msaToken.refresh_token = rsp.refresh_token;
m_data->msaToken.token = rsp.access_token;
emit finished(AccountTaskState::STATE_WORKING, tr("Got"));
}

View File

@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QObject>
#include <QTimer>
#include "minecraft/auth/AuthStep.h"
#include "net/Upload.h"
class MSADeviceCodeStep : public AuthStep {
Q_OBJECT
public:
explicit MSADeviceCodeStep(AccountData* data);
virtual ~MSADeviceCodeStep() noexcept = default;
void perform() override;
QString describe() override;
public slots:
void abort();
signals:
void authorizeWithBrowser(QString url, QString code, int expiresIn);
private slots:
void deviceAutorizationFinished();
void startPoolTimer();
void authenticateUser();
void authenticationFinished();
private:
QString m_clientId;
QString m_device_code;
bool m_is_aborted = false;
int interval = 5;
QTimer m_pool_timer;
QTimer m_expiration_timer;
std::shared_ptr<QByteArray> m_response;
Net::Upload::Ptr m_task;
};

View File

@ -35,123 +35,74 @@
#include "MSAStep.h" #include "MSAStep.h"
#include <QtNetworkAuth/qoauthhttpserverreplyhandler.h>
#include <QAbstractOAuth2>
#include <QNetworkRequest> #include <QNetworkRequest>
#include "BuildConfig.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "Application.h" #include "Application.h"
#include "Logging.h"
using OAuth2 = Katabasis::DeviceFlow; MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(silent)
using Activity = Katabasis::Activity;
MSAStep::MSAStep(AccountData* data, Action action) : AuthStep(data), m_action(action)
{ {
m_clientId = APPLICATION->getMSAClientID(); m_clientId = APPLICATION->getMSAClientID();
OAuth2::Options opts;
opts.scope = "XboxLive.signin offline_access";
opts.clientIdentifier = m_clientId;
opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
// FIXME: OAuth2 is not aware of our fancy shared pointers auto replyHandler = new QOAuthHttpServerReplyHandler(1337, this);
m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get()); replyHandler->setCallbackText(
" <iframe src=\"https://prismlauncher.org/successful-login\" title=\"PrismLauncher Microsoft login\" style=\"position:fixed; "
"top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden; "
"z-index:999999;\"/> ");
oauth2.setReplyHandler(replyHandler);
oauth2.setAuthorizationUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"));
oauth2.setAccessTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token"));
oauth2.setScope("XboxLive.SignIn XboxLive.offline_access");
oauth2.setClientIdentifier(m_clientId);
oauth2.setNetworkAccessManager(APPLICATION->network().get());
connect(m_oauth2, &OAuth2::activityChanged, this, &MSAStep::onOAuthActivityChanged); connect(&oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this] {
connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &MSAStep::showVerificationUriAndCode); m_data->msaClientID = oauth2.clientIdentifier();
m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc();
m_data->msaToken.notAfter = oauth2.expirationAt();
m_data->msaToken.extra = oauth2.extraTokens();
m_data->msaToken.refresh_token = oauth2.refreshToken();
m_data->msaToken.token = oauth2.token();
emit finished(AccountTaskState::STATE_WORKING, tr("Got "));
});
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &MSAStep::authorizeWithBrowser);
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this, [this](const QAbstractOAuth2::Error err) {
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
});
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::extraTokensChanged, this,
[this](const QVariantMap& tokens) { m_data->msaToken.extra = tokens; });
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::clientIdentifierChanged, this,
[this](const QString& clientIdentifier) { m_data->msaClientID = clientIdentifier; });
} }
MSAStep::~MSAStep() noexcept = default;
QString MSAStep::describe() QString MSAStep::describe()
{ {
return tr("Logging in with Microsoft account."); return tr("Logging in with Microsoft account.");
} }
void MSAStep::rehydrate()
{
switch (m_action) {
case Refresh: {
// TODO: check the tokens and see if they are old (older than a day)
return;
}
case Login: {
// NOOP
return;
}
}
}
void MSAStep::perform() void MSAStep::perform()
{ {
switch (m_action) { if (m_silent) {
case Refresh: { if (m_data->msaClientID != m_clientId) {
if (m_data->msaClientID != m_clientId) { emit finished(AccountTaskState::STATE_DISABLED,
emit hideVerificationUriAndCode(); tr("Microsoft user authentication failed - client identification has changed."));
emit finished(AccountTaskState::STATE_DISABLED,
tr("Microsoft user authentication failed - client identification has changed."));
}
m_oauth2->refresh();
return;
} }
case Login: { oauth2.setRefreshToken(m_data->msaToken.refresh_token);
QVariantMap extraOpts; oauth2.refreshAccessToken();
extraOpts["prompt"] = "select_account"; } else {
m_oauth2->setExtraRequestParams(extraOpts); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) // QMultiMap param changed in 6.0
oauth2.setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMultiMap<QString, QVariant>* map) {
#else
oauth2.setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMap<QString, QVariant>* map) {
#endif
map->insert("prompt", "select_account");
});
*m_data = AccountData(); *m_data = AccountData();
m_data->msaClientID = m_clientId; m_data->msaClientID = m_clientId;
m_oauth2->login(); oauth2.grant();
return;
}
}
}
void MSAStep::onOAuthActivityChanged(Katabasis::Activity activity)
{
switch (activity) {
case Katabasis::Activity::Idle:
case Katabasis::Activity::LoggingIn:
case Katabasis::Activity::Refreshing:
case Katabasis::Activity::LoggingOut: {
// We asked it to do something, it's doing it. Nothing to act upon.
return;
}
case Katabasis::Activity::Succeeded: {
// Succeeded or did not invalidate tokens
emit hideVerificationUriAndCode();
QVariantMap extraTokens = m_oauth2->extraTokens();
if (!extraTokens.isEmpty()) {
qCDebug(authCredentials()) << "Extra tokens in response:";
foreach (QString key, extraTokens.keys()) {
qCDebug(authCredentials()) << "\t" << key << ":" << extraTokens.value(key);
}
}
emit finished(AccountTaskState::STATE_WORKING, tr("Got "));
return;
}
case Katabasis::Activity::FailedSoft: {
// NOTE: soft error in the first step means 'offline'
emit hideVerificationUriAndCode();
emit finished(AccountTaskState::STATE_OFFLINE, tr("Microsoft user authentication ended with a network error."));
return;
}
case Katabasis::Activity::FailedGone: {
emit hideVerificationUriAndCode();
emit finished(AccountTaskState::STATE_FAILED_GONE, tr("Microsoft user authentication failed - user no longer exists."));
return;
}
case Katabasis::Activity::FailedHard: {
emit hideVerificationUriAndCode();
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
return;
}
default: {
emit hideVerificationUriAndCode();
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result."));
return;
}
} }
} }

View File

@ -36,30 +36,24 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include <katabasis/DeviceFlow.h> #include <QtNetworkAuth/qoauth2authorizationcodeflow.h>
class MSAStep : public AuthStep { class MSAStep : public AuthStep {
Q_OBJECT Q_OBJECT
public: public:
enum Action { Refresh, Login }; explicit MSAStep(AccountData* data, bool silent = false);
virtual ~MSAStep() noexcept = default;
public:
explicit MSAStep(AccountData* data, Action action);
virtual ~MSAStep() noexcept;
void perform() override; void perform() override;
void rehydrate() override;
QString describe() override; QString describe() override;
private slots: signals:
void onOAuthActivityChanged(Katabasis::Activity activity); void authorizeWithBrowser(const QUrl& url);
private: private:
Katabasis::DeviceFlow* m_oauth2 = nullptr; bool m_silent;
Action m_action;
QString m_clientId; QString m_clientId;
QOAuth2AuthorizationCodeFlow oauth2;
}; };

View File

@ -2,15 +2,13 @@
#include <QNetworkRequest> #include <QNetworkRequest>
#include "Logging.h" #include "Application.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h" #include "net/NetUtils.h"
#include "net/StaticHeaderProxy.h"
MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) {} MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) {}
MinecraftProfileStep::~MinecraftProfileStep() noexcept = default;
QString MinecraftProfileStep::describe() QString MinecraftProfileStep::describe()
{ {
return tr("Fetching the Minecraft profile."); return tr("Fetching the Minecraft profile.");
@ -18,52 +16,47 @@ QString MinecraftProfileStep::describe()
void MinecraftProfileStep::perform() void MinecraftProfileStep::perform()
{ {
auto url = QUrl("https://api.minecraftservices.com/minecraft/profile"); QUrl url("https://api.minecraftservices.com/minecraft/profile");
QNetworkRequest request = QNetworkRequest(url); auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" },
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); { "Accept", "application/json" },
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } };
AuthRequest* requestor = new AuthRequest(this); m_response.reset(new QByteArray());
connect(requestor, &AuthRequest::finished, this, &MinecraftProfileStep::onRequestDone); m_task = Net::Download::makeByteArray(url, m_response);
requestor->get(request); m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_task.get(), &Task::finished, this, &MinecraftProfileStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start();
} }
void MinecraftProfileStep::rehydrate() void MinecraftProfileStep::onRequestDone()
{ {
// NOOP, for now. We only save bools and there's nothing to check. if (m_task->error() == QNetworkReply::ContentNotFoundError) {
}
void MinecraftProfileStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers)
{
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
requestor->deleteLater();
qCDebug(authCredentials()) << data;
if (error == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state. // NOTE: Succeed even if we do not have a profile. This is a valid account state.
m_data->minecraftProfile = MinecraftProfile(); m_data->minecraftProfile = MinecraftProfile();
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Account has no Minecraft profile.")); emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Account has no Minecraft profile."));
return; return;
} }
if (error != QNetworkReply::NoError) { if (m_task->error() != QNetworkReply::NoError) {
qWarning() << "Error getting profile:"; qWarning() << "Error getting profile:";
qWarning() << " HTTP Status: " << requestor->httpStatus_; qWarning() << " HTTP Status: " << m_task->replyStatusCode();
qWarning() << " Internal error no.: " << error; qWarning() << " Internal error no.: " << m_task->error();
qWarning() << " Error string: " << requestor->errorString_; qWarning() << " Error string: " << m_task->errorString();
qWarning() << " Response:"; qWarning() << " Response:";
qWarning() << QString::fromUtf8(data); qWarning() << QString::fromUtf8(*m_response);
if (Net::isApplicationError(error)) { if (Net::isApplicationError(m_task->error())) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)); tr("Minecraft Java profile acquisition failed: %1").arg(m_task->errorString()));
} else { } else {
emit finished(AccountTaskState::STATE_OFFLINE, emit finished(AccountTaskState::STATE_OFFLINE, tr("Minecraft Java profile acquisition failed: %1").arg(m_task->errorString()));
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_));
} }
return; return;
} }
if (!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) { if (!Parsers::parseMinecraftProfile(*m_response, m_data->minecraftProfile)) {
m_data->minecraftProfile = MinecraftProfile(); m_data->minecraftProfile = MinecraftProfile();
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile response could not be parsed")); emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile response could not be parsed"));
return; return;

View File

@ -1,21 +1,25 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <memory>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/Download.h"
class MinecraftProfileStep : public AuthStep { class MinecraftProfileStep : public AuthStep {
Q_OBJECT Q_OBJECT
public: public:
explicit MinecraftProfileStep(AccountData* data); explicit MinecraftProfileStep(AccountData* data);
virtual ~MinecraftProfileStep() noexcept; virtual ~MinecraftProfileStep() noexcept = default;
void perform() override; void perform() override;
void rehydrate() override;
QString describe() override; QString describe() override;
private slots: private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); void onRequestDone();
private:
std::shared_ptr<QByteArray> m_response;
Net::Download::Ptr m_task;
}; };

View File

@ -1,21 +0,0 @@
#include "OfflineStep.h"
#include "Application.h"
OfflineStep::OfflineStep(AccountData* data) : AuthStep(data) {}
OfflineStep::~OfflineStep() noexcept = default;
QString OfflineStep::describe()
{
return tr("Creating offline account.");
}
void OfflineStep::rehydrate()
{
// NOOP
}
void OfflineStep::perform()
{
emit finished(AccountTaskState::STATE_WORKING, tr("Created offline account."));
}

View File

@ -1,19 +0,0 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
#include <katabasis/DeviceFlow.h>
class OfflineStep : public AuthStep {
Q_OBJECT
public:
explicit OfflineStep(AccountData* data);
virtual ~OfflineStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
};

View File

@ -4,27 +4,22 @@
#include <QJsonParseError> #include <QJsonParseError>
#include <QNetworkRequest> #include <QNetworkRequest>
#include "Application.h"
#include "Logging.h" #include "Logging.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h" #include "net/NetUtils.h"
#include "net/StaticHeaderProxy.h"
#include "net/Upload.h"
XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Katabasis::Token* token, QString relyingParty, QString authorizationKind) XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Token* token, QString relyingParty, QString authorizationKind)
: AuthStep(data), m_token(token), m_relyingParty(relyingParty), m_authorizationKind(authorizationKind) : AuthStep(data), m_token(token), m_relyingParty(relyingParty), m_authorizationKind(authorizationKind)
{} {}
XboxAuthorizationStep::~XboxAuthorizationStep() noexcept = default;
QString XboxAuthorizationStep::describe() QString XboxAuthorizationStep::describe()
{ {
return tr("Getting authorization to access %1 services.").arg(m_authorizationKind); return tr("Getting authorization to access %1 services.").arg(m_authorizationKind);
} }
void XboxAuthorizationStep::rehydrate()
{
// FIXME: check if the tokens are good?
}
void XboxAuthorizationStep::perform() void XboxAuthorizationStep::perform()
{ {
QString xbox_auth_template = R"XXX( QString xbox_auth_template = R"XXX(
@ -41,40 +36,44 @@ void XboxAuthorizationStep::perform()
)XXX"; )XXX";
auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token, m_relyingParty); auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token, m_relyingParty);
// http://xboxlive.com // http://xboxlive.com
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize")); QUrl url("https://xsts.auth.xboxlive.com/xsts/authorize");
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); auto headers = QList<Net::HeaderPair>{
request.setRawHeader("Accept", "application/json"); { "Content-Type", "application/json" },
AuthRequest* requestor = new AuthRequest(this); { "Accept", "application/json" },
connect(requestor, &AuthRequest::finished, this, &XboxAuthorizationStep::onRequestDone); };
requestor->post(request, xbox_auth_data.toUtf8()); m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8());
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_task.get(), &Task::finished, this, &XboxAuthorizationStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start();
qDebug() << "Getting authorization token for " << m_relyingParty; qDebug() << "Getting authorization token for " << m_relyingParty;
} }
void XboxAuthorizationStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) void XboxAuthorizationStep::onRequestDone()
{ {
auto requestor = qobject_cast<AuthRequest*>(QObject::sender()); qCDebug(authCredentials()) << *m_response;
requestor->deleteLater(); if (m_task->error() != QNetworkReply::NoError) {
qWarning() << "Reply error:" << m_task->error();
qCDebug(authCredentials()) << data; if (Net::isApplicationError(m_task->error())) {
if (error != QNetworkReply::NoError) { if (!processSTSError()) {
qWarning() << "Reply error:" << error;
if (Net::isApplicationError(error)) {
if (!processSTSError(error, data, headers)) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, error)); tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, m_task->error()));
} else { } else {
emit finished(AccountTaskState::STATE_FAILED_SOFT, emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, requestor->errorString_)); tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, m_task->errorString()));
} }
} else { } else {
emit finished(AccountTaskState::STATE_OFFLINE, emit finished(AccountTaskState::STATE_OFFLINE,
tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, requestor->errorString_)); tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, m_task->errorString()));
} }
return; return;
} }
Katabasis::Token temp; Token temp;
if (!Parsers::parseXTokenResponse(data, temp, m_authorizationKind)) { if (!Parsers::parseXTokenResponse(*m_response, temp, m_authorizationKind)) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Could not parse authorization response for access to %1 services.").arg(m_authorizationKind)); tr("Could not parse authorization response for access to %1 services.").arg(m_authorizationKind));
return; return;
@ -91,11 +90,11 @@ void XboxAuthorizationStep::onRequestDone(QNetworkReply::NetworkError error, QBy
emit finished(AccountTaskState::STATE_WORKING, tr("Got authorization to access %1").arg(m_relyingParty)); emit finished(AccountTaskState::STATE_WORKING, tr("Got authorization to access %1").arg(m_relyingParty));
} }
bool XboxAuthorizationStep::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) bool XboxAuthorizationStep::processSTSError()
{ {
if (error == QNetworkReply::AuthenticationRequiredError) { if (m_task->error() == QNetworkReply::AuthenticationRequiredError) {
QJsonParseError jsonError; QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); QJsonDocument doc = QJsonDocument::fromJson(*m_response, &jsonError);
if (jsonError.error) { if (jsonError.error) {
qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString(); qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString();
emit finished(AccountTaskState::STATE_FAILED_SOFT, emit finished(AccountTaskState::STATE_FAILED_SOFT,
@ -126,7 +125,35 @@ bool XboxAuthorizationStep::processSTSError(QNetworkReply::NetworkError error, Q
emit finished( emit finished(
AccountTaskState::STATE_FAILED_SOFT, AccountTaskState::STATE_FAILED_SOFT,
tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.") tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.")
.arg("<a href=\"https://help.minecraft.net/hc/en-us/articles/4403181904525\">help.minecraft.net</a>")); .arg("<a href=\"https://help.minecraft.net/hc/en-us/articles/4408968616077\">help.minecraft.net</a>"));
return true;
}
// the following codes where copied from: https://github.com/PrismarineJS/prismarine-auth/pull/44
case 2148916236: {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("This Microsoft account requires proof of age to play. Please login to %1 to provide proof of age.")
.arg("<a href=\"https://login.live.com/login.srf\">login.live.com</a>"));
return true;
}
case 2148916237:
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account has reached its limit for playtime. This "
"Microsoft account has been blocked from logging in."));
return true;
case 2148916227: {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account was banned by Xbox for violating one or more "
"Community Standards for Xbox and is unable to be used."));
return true;
}
case 2148916229: {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("This Microsoft account is currently restricted and your guardian has not given you permission to play "
"online. Login to %1 and have your guardian change your permissions.")
.arg("<a href=\"https://account.microsoft.com/family/\">account.microsoft.com</a>"));
return true;
}
case 2148916234: {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("This Microsoft account has not accepted Xbox's Terms of Service. Please login and accept them."));
return true; return true;
} }
default: { default: {

View File

@ -1,29 +1,32 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <memory>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/Upload.h"
class XboxAuthorizationStep : public AuthStep { class XboxAuthorizationStep : public AuthStep {
Q_OBJECT Q_OBJECT
public: public:
explicit XboxAuthorizationStep(AccountData* data, Katabasis::Token* token, QString relyingParty, QString authorizationKind); explicit XboxAuthorizationStep(AccountData* data, Token* token, QString relyingParty, QString authorizationKind);
virtual ~XboxAuthorizationStep() noexcept; virtual ~XboxAuthorizationStep() noexcept = default;
void perform() override; void perform() override;
void rehydrate() override;
QString describe() override; QString describe() override;
private: private:
bool processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers); bool processSTSError();
private slots: private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); void onRequestDone();
private: private:
Katabasis::Token* m_token; Token* m_token;
QString m_relyingParty; QString m_relyingParty;
QString m_authorizationKind; QString m_authorizationKind;
std::shared_ptr<QByteArray> m_response;
Net::Upload::Ptr m_task;
}; };

View File

@ -3,28 +3,21 @@
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QUrlQuery> #include <QUrlQuery>
#include "Application.h"
#include "Logging.h" #include "Logging.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h" #include "net/NetUtils.h"
#include "net/StaticHeaderProxy.h"
XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) {} XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) {}
XboxProfileStep::~XboxProfileStep() noexcept = default;
QString XboxProfileStep::describe() QString XboxProfileStep::describe()
{ {
return tr("Fetching Xbox profile."); return tr("Fetching Xbox profile.");
} }
void XboxProfileStep::rehydrate()
{
// NOOP, for now. We only save bools and there's nothing to check.
}
void XboxProfileStep::perform() void XboxProfileStep::perform()
{ {
auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings"); QUrl url("https://profile.xboxlive.com/users/me/profile/settings");
QUrlQuery q; QUrlQuery q;
q.addQueryItem("settings", q.addQueryItem("settings",
"GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw," "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
@ -33,36 +26,38 @@ void XboxProfileStep::perform()
"PreferredColor,Location,Bio,Watermarks," "PreferredColor,Location,Bio,Watermarks,"
"RealName,RealNameOverride,IsQuarantined"); "RealName,RealNameOverride,IsQuarantined");
url.setQuery(q); url.setQuery(q);
auto headers = QList<Net::HeaderPair>{
{ "Content-Type", "application/json" },
{ "Accept", "application/json" },
{ "x-xbl-contract-version", "3" },
{ "Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8() }
};
QNetworkRequest request = QNetworkRequest(url); m_response.reset(new QByteArray());
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); m_task = Net::Download::makeByteArray(url, m_response);
request.setRawHeader("Accept", "application/json"); m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
request.setRawHeader("x-xbl-contract-version", "3");
request.setRawHeader("Authorization", connect(m_task.get(), &Task::finished, this, &XboxProfileStep::onRequestDone);
QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8());
AuthRequest* requestor = new AuthRequest(this); m_task->setNetwork(APPLICATION->network());
connect(requestor, &AuthRequest::finished, this, &XboxProfileStep::onRequestDone); m_task->start();
requestor->get(request);
qDebug() << "Getting Xbox profile..."; qDebug() << "Getting Xbox profile...";
} }
void XboxProfileStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) void XboxProfileStep::onRequestDone()
{ {
auto requestor = qobject_cast<AuthRequest*>(QObject::sender()); if (m_task->error() != QNetworkReply::NoError) {
requestor->deleteLater(); qWarning() << "Reply error:" << m_task->error();
qCDebug(authCredentials()) << *m_response;
if (error != QNetworkReply::NoError) { if (Net::isApplicationError(m_task->error())) {
qWarning() << "Reply error:" << error; emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to retrieve the Xbox profile: %1").arg(m_task->errorString()));
qCDebug(authCredentials()) << data;
if (Net::isApplicationError(error)) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to retrieve the Xbox profile: %1").arg(requestor->errorString_));
} else { } else {
emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to retrieve the Xbox profile: %1").arg(requestor->errorString_)); emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to retrieve the Xbox profile: %1").arg(m_task->errorString()));
} }
return; return;
} }
qCDebug(authCredentials()) << "XBox profile: " << data; qCDebug(authCredentials()) << "XBox profile: " << *m_response;
emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile")); emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile"));
} }

View File

@ -1,21 +1,25 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <memory>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/Download.h"
class XboxProfileStep : public AuthStep { class XboxProfileStep : public AuthStep {
Q_OBJECT Q_OBJECT
public: public:
explicit XboxProfileStep(AccountData* data); explicit XboxProfileStep(AccountData* data);
virtual ~XboxProfileStep() noexcept; virtual ~XboxProfileStep() noexcept = default;
void perform() override; void perform() override;
void rehydrate() override;
QString describe() override; QString describe() override;
private slots: private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); void onRequestDone();
private:
std::shared_ptr<QByteArray> m_response;
Net::Download::Ptr m_task;
}; };

View File

@ -2,24 +2,18 @@
#include <QNetworkRequest> #include <QNetworkRequest>
#include "minecraft/auth/AuthRequest.h" #include "Application.h"
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h" #include "net/NetUtils.h"
#include "net/StaticHeaderProxy.h"
XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) {} XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) {}
XboxUserStep::~XboxUserStep() noexcept = default;
QString XboxUserStep::describe() QString XboxUserStep::describe()
{ {
return tr("Logging in as an Xbox user."); return tr("Logging in as an Xbox user.");
} }
void XboxUserStep::rehydrate()
{
// NOOP, for now. We only save bools and there's nothing to check.
}
void XboxUserStep::perform() void XboxUserStep::perform()
{ {
QString xbox_auth_template = R"XXX( QString xbox_auth_template = R"XXX(
@ -35,36 +29,39 @@ void XboxUserStep::perform()
)XXX"; )XXX";
auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token); auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token);
QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate")); QUrl url("https://user.auth.xboxlive.com/user/authenticate");
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); auto headers = QList<Net::HeaderPair>{
request.setRawHeader("Accept", "application/json"); { "Content-Type", "application/json" },
// set contract-version header (prevent err 400 bad-request?) { "Accept", "application/json" },
// https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders // set contract-version header (prevent err 400 bad-request?)
request.setRawHeader("x-xbl-contract-version", "1"); // https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders
{ "x-xbl-contract-version", "1" }
};
m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8());
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
auto* requestor = new AuthRequest(this); connect(m_task.get(), &Task::finished, this, &XboxUserStep::onRequestDone);
connect(requestor, &AuthRequest::finished, this, &XboxUserStep::onRequestDone);
requestor->post(request, xbox_auth_data.toUtf8()); m_task->setNetwork(APPLICATION->network());
m_task->start();
qDebug() << "First layer of XBox auth ... commencing."; qDebug() << "First layer of XBox auth ... commencing.";
} }
void XboxUserStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) void XboxUserStep::onRequestDone()
{ {
auto requestor = qobject_cast<AuthRequest*>(QObject::sender()); if (m_task->error() != QNetworkReply::NoError) {
requestor->deleteLater(); qWarning() << "Reply error:" << m_task->error();
if (Net::isApplicationError(m_task->error())) {
if (error != QNetworkReply::NoError) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed: %1").arg(m_task->errorString()));
qWarning() << "Reply error:" << error;
if (Net::isApplicationError(error)) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed: %1").arg(requestor->errorString_));
} else { } else {
emit finished(AccountTaskState::STATE_OFFLINE, tr("XBox user authentication failed: %1").arg(requestor->errorString_)); emit finished(AccountTaskState::STATE_OFFLINE, tr("XBox user authentication failed: %1").arg(m_task->errorString()));
} }
return; return;
} }
Katabasis::Token temp; Token temp;
if (!Parsers::parseXTokenResponse(data, temp, "UToken")) { if (!Parsers::parseXTokenResponse(*m_response, temp, "UToken")) {
qWarning() << "Could not parse user authentication response..."; qWarning() << "Could not parse user authentication response...";
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication response could not be understood.")); emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication response could not be understood."));
return; return;

View File

@ -1,21 +1,25 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <memory>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/Upload.h"
class XboxUserStep : public AuthStep { class XboxUserStep : public AuthStep {
Q_OBJECT Q_OBJECT
public: public:
explicit XboxUserStep(AccountData* data); explicit XboxUserStep(AccountData* data);
virtual ~XboxUserStep() noexcept; virtual ~XboxUserStep() noexcept = default;
void perform() override; void perform() override;
void rehydrate() override;
QString describe() override; QString describe() override;
private slots: private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); void onRequestDone();
private:
std::shared_ptr<QByteArray> m_response;
Net::Upload::Ptr m_task;
}; };

View File

@ -79,6 +79,7 @@ void ExtractNatives::executeTask()
auto settings = minecraftInstance->settings(); auto settings = minecraftInstance->settings();
auto outputPath = minecraftInstance->getNativePath(); auto outputPath = minecraftInstance->getNativePath();
FS::ensureFolderPathExists(outputPath);
auto javaVersion = minecraftInstance->getJavaVersion(); auto javaVersion = minecraftInstance->getJavaVersion();
bool jniHackEnabled = javaVersion.major() >= 8; bool jniHackEnabled = javaVersion.major() >= 8;
for (const auto& source : toExtract) { for (const auto& source : toExtract) {

View File

@ -16,8 +16,6 @@
#pragma once #pragma once
#include <launch/LaunchStep.h> #include <launch/LaunchStep.h>
#include <memory>
#include "minecraft/auth/AuthSession.h"
// FIXME: temporary wrapper for existing task. // FIXME: temporary wrapper for existing task.
class ExtractNatives : public LaunchStep { class ExtractNatives : public LaunchStep {

View File

@ -66,32 +66,6 @@ LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) : LaunchStep(parent)
connect(&m_process, &LoggedProcess::stateChanged, this, &LauncherPartLaunch::on_state); connect(&m_process, &LoggedProcess::stateChanged, this, &LauncherPartLaunch::on_state);
} }
#ifdef Q_OS_WIN
// returns 8.3 file format from long path
#include <windows.h>
QString shortPathName(const QString& file)
{
auto input = file.toStdWString();
std::wstring output;
long length = GetShortPathNameW(input.c_str(), NULL, 0);
// 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);
GetShortPathNameW(input.c_str(), (LPWSTR)output.c_str(), length);
output.resize(length - 1);
QString ret = QString::fromStdWString(output);
return ret;
}
#endif
// if the string survives roundtrip through local 8bit encoding...
bool fitsInLocal8bit(const QString& string)
{
return string == QString::fromLocal8Bit(string.toLocal8Bit());
}
void LauncherPartLaunch::executeTask() void LauncherPartLaunch::executeTask()
{ {
QString jarPath = APPLICATION->getJarPath("NewLaunch.jar"); QString jarPath = APPLICATION->getJarPath("NewLaunch.jar");
@ -136,24 +110,15 @@ void LauncherPartLaunch::executeTask()
auto natPath = minecraftInstance->getNativePath(); auto natPath = minecraftInstance->getNativePath();
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
if (!fitsInLocal8bit(natPath)) { natPath = FS::getPathNameInLocal8bit(natPath);
args << "-Djava.library.path=" + shortPathName(natPath);
} else {
args << "-Djava.library.path=" + natPath;
}
#else
args << "-Djava.library.path=" + natPath;
#endif #endif
args << "-Djava.library.path=" + natPath;
args << "-cp"; args << "-cp";
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
QStringList processed; QStringList processed;
for (auto& item : classPath) { for (auto& item : classPath) {
if (!fitsInLocal8bit(item)) { processed << FS::getPathNameInLocal8bit(item);
processed << shortPathName(item);
} else {
processed << item;
}
} }
args << processed.join(';'); args << processed.join(';');
#else #else

View File

@ -276,7 +276,7 @@ QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const
return {}; return {};
if (m_pack_image_cache_key.was_ever_used) { if (m_pack_image_cache_key.was_ever_used) {
qDebug() << "Mod" << name() << "Had it's icon evicted form the cache. reloading..."; qDebug() << "Mod" << name() << "Had it's icon evicted from the cache. reloading...";
PixmapCache::markCacheMissByEviciton(); PixmapCache::markCacheMissByEviciton();
} }
// Image got evicted from the cache or an attempt to load it has not been made. load it and retry. // Image got evicted from the cache or an attempt to load it has not been made. load it and retry.

View File

@ -285,7 +285,12 @@ void ResourceFolderModel::resolveResource(Resource* res)
connect( connect(
task.get(), &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); task.get(), &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
connect( connect(
task.get(), &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection); task.get(), &Task::finished, this,
[=] {
m_active_parse_tasks.remove(ticket);
emit parseFinished();
},
Qt::ConnectionType::QueuedConnection);
m_helper_thread_task.addTask(task); m_helper_thread_task.addTask(task);
@ -623,3 +628,26 @@ QString ResourceFolderModel::instDirPath() const
{ {
return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); return QFileInfo(m_instance->instanceRoot()).absoluteFilePath();
} }
void ResourceFolderModel::onParseFailed(int ticket, QString resource_id)
{
auto iter = m_active_parse_tasks.constFind(ticket);
if (iter == m_active_parse_tasks.constEnd())
return;
auto removed_index = m_resources_index[resource_id];
auto removed_it = m_resources.begin() + removed_index;
Q_ASSERT(removed_it != m_resources.end());
beginRemoveRows(QModelIndex(), removed_index, removed_index);
m_resources.erase(removed_it);
// update index
m_resources_index.clear();
int idx = 0;
for (auto const& mod : qAsConst(m_resources)) {
m_resources_index[mod->internal_id()] = idx;
idx++;
}
endRemoveRows();
}

View File

@ -143,6 +143,7 @@ class ResourceFolderModel : public QAbstractListModel {
signals: signals:
void updateFinished(); void updateFinished();
void parseFinished();
protected: protected:
/** This creates a new update task to be executed by update(). /** This creates a new update task to be executed by update().
@ -189,11 +190,7 @@ class ResourceFolderModel : public QAbstractListModel {
* if the resource is complex and has more stuff to parse. * if the resource is complex and has more stuff to parse.
*/ */
virtual void onParseSucceeded(int ticket, QString resource_id); virtual void onParseSucceeded(int ticket, QString resource_id);
virtual void onParseFailed(int ticket, QString resource_id) virtual void onParseFailed(int ticket, QString resource_id);
{
Q_UNUSED(ticket);
Q_UNUSED(resource_id);
}
protected: protected:
// Represents the relationship between a column's index (represented by the list index), and it's sorting key. // Represents the relationship between a column's index (represented by the list index), and it's sorting key.
@ -307,7 +304,6 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
auto removed_it = m_resources.begin() + removed_index; auto removed_it = m_resources.begin() + removed_index;
Q_ASSERT(removed_it != m_resources.end()); Q_ASSERT(removed_it != m_resources.end());
Q_ASSERT(removed_set.contains(removed_it->get()->internal_id()));
if ((*removed_it)->isResolving()) { if ((*removed_it)->isResolving()) {
auto ticket = (*removed_it)->resolutionTicket(); auto ticket = (*removed_it)->resolutionTicket();

View File

@ -35,8 +35,3 @@ bool ShaderPack::valid() const
{ {
return m_pack_format != ShaderPackFormat::INVALID; return m_pack_format != ShaderPackFormat::INVALID;
} }
bool ShaderPack::applyFilter(QRegularExpression filter) const
{
return valid() && Resource::applyFilter(filter);
}

View File

@ -54,7 +54,6 @@ class ShaderPack : public Resource {
void setPackFormat(ShaderPackFormat new_format); void setPackFormat(ShaderPackFormat new_format);
bool valid() const override; bool valid() const override;
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
protected: protected:
mutable QMutex m_data_lock; mutable QMutex m_data_lock;

View File

@ -57,9 +57,11 @@ GetModDependenciesTask::GetModDependenciesTask(QObject* parent,
, m_version(mcVersion(instance)) , m_version(mcVersion(instance))
, m_loaderType(mcLoaders(instance)) , m_loaderType(mcLoaders(instance))
{ {
for (auto mod : folder->allMods()) for (auto mod : folder->allMods()) {
m_mods_file_names << mod->fileinfo().fileName();
if (auto meta = mod->metadata(); meta) if (auto meta = mod->metadata(); meta)
m_mods.append(meta); m_mods.append(meta);
}
prepare(); prepare();
} }
@ -182,7 +184,9 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen
ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType }; ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType };
ResourceAPI::DependencySearchCallbacks callbacks; ResourceAPI::DependencySearchCallbacks callbacks;
callbacks.on_fail = [](QString reason, int) {
qCritical() << tr("A network error occurred. Could not load project dependencies:%1").arg(reason);
};
callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, [[maybe_unused]] auto& pack) { callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, [[maybe_unused]] auto& pack) {
try { try {
QJsonArray arr; QJsonArray arr;
@ -229,8 +233,13 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen
if (dep_.addonId != pDep->version.addonId) { if (dep_.addonId != pDep->version.addonId) {
removePack(pDep->version.addonId); removePack(pDep->version.addonId);
addTask(prepareDependencyTask(dep_, provider.name, level)); addTask(prepareDependencyTask(dep_, provider.name, level));
} else } else {
addTask(getProjectInfoTask(pDep)); addTask(getProjectInfoTask(pDep));
}
}
if (isLocalyInstalled(pDep)) {
removePack(pDep->version.addonId);
return;
} }
for (auto dep_ : getDependenciesForVersion(pDep->version, provider.name)) { for (auto dep_ : getDependenciesForVersion(pDep->version, provider.name)) {
addTask(prepareDependencyTask(dep_, provider.name, level - 1)); addTask(prepareDependencyTask(dep_, provider.name, level - 1));
@ -256,9 +265,9 @@ void GetModDependenciesTask::removePack(const QVariant& addonId)
#endif #endif
} }
QHash<QString, QStringList> GetModDependenciesTask::getRequiredBy() auto GetModDependenciesTask::getExtraInfo() -> QHash<QString, PackDependencyExtraInfo>
{ {
QHash<QString, QStringList> rby; QHash<QString, PackDependencyExtraInfo> rby;
auto fullList = m_selected + m_pack_dependencies; auto fullList = m_selected + m_pack_dependencies;
for (auto& mod : fullList) { for (auto& mod : fullList) {
auto addonId = mod->pack->addonId; auto addonId = mod->pack->addonId;
@ -280,7 +289,61 @@ QHash<QString, QStringList> GetModDependenciesTask::getRequiredBy()
req.append(smod->pack->name); req.append(smod->pack->name);
} }
} }
rby[addonId.toString()] = req; rby[addonId.toString()] = { maybeInstalled(mod), req };
} }
return rby; return rby;
} }
// super lax compare (but not fuzzy)
// convert to lowercase
// convert all speratores to whitespace
// simplify sequence of internal whitespace to a single space
// efectivly compare two strings ignoring all separators and case
auto laxCompare = [](QString fsfilename, QString metadataFilename, bool excludeDigits = false) {
// allowed character seperators
QList<QChar> allowedSeperators = { '-', '+', '.', '_' };
if (excludeDigits)
allowedSeperators.append({ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' });
// copy in lowercase
auto fsName = fsfilename.toLower();
auto metaName = metadataFilename.toLower();
// replace all potential allowed seperatores with whitespace
for (auto sep : allowedSeperators) {
fsName = fsName.replace(sep, ' ');
metaName = metaName.replace(sep, ' ');
}
// remove extraneous whitespace
fsName = fsName.simplified();
metaName = metaName.simplified();
return fsName.compare(metaName) == 0;
};
bool GetModDependenciesTask::isLocalyInstalled(std::shared_ptr<PackDependency> pDep)
{
return pDep->version.fileName.isEmpty() ||
std::find_if(m_selected.begin(), m_selected.end(),
[pDep](std::shared_ptr<PackDependency> i) {
return !i->version.fileName.isEmpty() && laxCompare(i->version.fileName, pDep->version.fileName);
}) != m_selected.end() || // check the selected versions
std::find_if(m_mods_file_names.begin(), m_mods_file_names.end(),
[pDep](QString i) { return !i.isEmpty() && laxCompare(i, pDep->version.fileName); }) !=
m_mods_file_names.end() || // check the existing mods
std::find_if(m_pack_dependencies.begin(), m_pack_dependencies.end(), [pDep](std::shared_ptr<PackDependency> i) {
return pDep->pack->addonId != i->pack->addonId && !i->version.fileName.isEmpty() &&
laxCompare(pDep->version.fileName, i->version.fileName);
}) != m_pack_dependencies.end(); // check loaded dependencies
}
bool GetModDependenciesTask::maybeInstalled(std::shared_ptr<PackDependency> pDep)
{
return std::find_if(m_mods_file_names.begin(), m_mods_file_names.end(), [pDep](QString i) {
return !i.isEmpty() && laxCompare(i, pDep->version.fileName, true);
}) != m_mods_file_names.end(); // check the existing mods
}

View File

@ -50,6 +50,11 @@ class GetModDependenciesTask : public SequentialTask {
} }
}; };
struct PackDependencyExtraInfo {
bool maybe_installed;
QStringList required_by;
};
struct Provider { struct Provider {
ModPlatform::ResourceProvider name; ModPlatform::ResourceProvider name;
std::shared_ptr<ResourceDownload::ModModel> mod; std::shared_ptr<ResourceDownload::ModModel> mod;
@ -62,7 +67,7 @@ class GetModDependenciesTask : public SequentialTask {
QList<std::shared_ptr<PackDependency>> selected); QList<std::shared_ptr<PackDependency>> selected);
auto getDependecies() const -> QList<std::shared_ptr<PackDependency>> { return m_pack_dependencies; } auto getDependecies() const -> QList<std::shared_ptr<PackDependency>> { return m_pack_dependencies; }
QHash<QString, QStringList> getRequiredBy(); QHash<QString, PackDependencyExtraInfo> getExtraInfo();
protected slots: protected slots:
Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, ModPlatform::ResourceProvider, int); Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, ModPlatform::ResourceProvider, int);
@ -73,10 +78,14 @@ class GetModDependenciesTask : public SequentialTask {
ModPlatform::Dependency getOverride(const ModPlatform::Dependency&, ModPlatform::ResourceProvider providerName); ModPlatform::Dependency getOverride(const ModPlatform::Dependency&, ModPlatform::ResourceProvider providerName);
void removePack(const QVariant& addonId); void removePack(const QVariant& addonId);
bool isLocalyInstalled(std::shared_ptr<PackDependency> pDep);
bool maybeInstalled(std::shared_ptr<PackDependency> pDep);
private: private:
QList<std::shared_ptr<PackDependency>> m_pack_dependencies; QList<std::shared_ptr<PackDependency>> m_pack_dependencies;
QList<std::shared_ptr<Metadata::ModStruct>> m_mods; QList<std::shared_ptr<Metadata::ModStruct>> m_mods;
QList<std::shared_ptr<PackDependency>> m_selected; QList<std::shared_ptr<PackDependency>> m_selected;
QStringList m_mods_file_names;
Provider m_flame_provider; Provider m_flame_provider;
Provider m_modrinth_provider; Provider m_modrinth_provider;

View File

@ -469,7 +469,7 @@ bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level)
QuaZipFile file(&zip); QuaZipFile file(&zip);
if (zip.setCurrentFile("META-INF/mods.toml")) { if (zip.setCurrentFile("META-INF/mods.toml") || zip.setCurrentFile("META-INF/neoforge.mods.toml")) {
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
zip.close(); zip.close();
return false; return false;
@ -746,7 +746,7 @@ void LocalModParseTask::executeTask()
m_result->details = mod.details(); m_result->details = mod.details();
if (m_aborted) if (m_aborted)
emit finished(); emitAborted();
else else
emitSucceeded(); emitSucceeded();
} }

View File

@ -286,8 +286,10 @@ bool LocalResourcePackParseTask::abort()
void LocalResourcePackParseTask::executeTask() void LocalResourcePackParseTask::executeTask()
{ {
if (!ResourcePackUtils::process(m_resource_pack)) if (!ResourcePackUtils::process(m_resource_pack)) {
emitFailed("this is not a resource pack");
return; return;
}
if (m_aborted) if (m_aborted)
emitAborted(); emitAborted();

View File

@ -103,8 +103,10 @@ bool LocalShaderPackParseTask::abort()
void LocalShaderPackParseTask::executeTask() void LocalShaderPackParseTask::executeTask()
{ {
if (!ShaderPackUtils::process(m_shader_pack)) if (!ShaderPackUtils::process(m_shader_pack)) {
emitFailed("this is not a shader pack");
return; return;
}
if (m_aborted) if (m_aborted)
emitAborted(); emitAborted();

View File

@ -241,8 +241,10 @@ bool LocalTexturePackParseTask::abort()
void LocalTexturePackParseTask::executeTask() void LocalTexturePackParseTask::executeTask()
{ {
if (!TexturePackUtils::process(m_texture_pack)) if (!TexturePackUtils::process(m_texture_pack)) {
emitFailed("this is not a texture pack");
return; return;
}
if (m_aborted) if (m_aborted)
emitAborted(); emitAborted();

View File

@ -28,6 +28,7 @@ class CheckUpdateTask : public Task {
QString changelog; QString changelog;
ModPlatform::ResourceProvider provider; ModPlatform::ResourceProvider provider;
shared_qobject_ptr<ResourceDownloadTask> download; shared_qobject_ptr<ResourceDownloadTask> download;
bool enabled = true;
public: public:
UpdatableMod(QString name, UpdatableMod(QString name,
@ -37,7 +38,8 @@ class CheckUpdateTask : public Task {
std::optional<ModPlatform::IndexedVersionType> new_v_type, std::optional<ModPlatform::IndexedVersionType> new_v_type,
QString changelog, QString changelog,
ModPlatform::ResourceProvider p, ModPlatform::ResourceProvider p,
shared_qobject_ptr<ResourceDownloadTask> t) shared_qobject_ptr<ResourceDownloadTask> t,
bool enabled = true)
: name(name) : name(name)
, old_hash(old_h) , old_hash(old_h)
, old_version(old_v) , old_version(old_v)
@ -46,6 +48,7 @@ class CheckUpdateTask : public Task {
, changelog(changelog) , changelog(changelog)
, provider(p) , provider(p)
, download(t) , download(t)
, enabled(enabled)
{} {}
}; };

View File

@ -149,6 +149,7 @@ void EnsureMetadataTask::executeTask()
if (m_current_task) if (m_current_task)
m_current_task.reset(); m_current_task.reset();
}); });
connect(project_task.get(), &Task::failed, this, &EnsureMetadataTask::emitFailed);
m_current_task = project_task; m_current_task = project_task;
project_task->start(); project_task->start();

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