diff --git a/.github/scripts/prepare_JREs.sh b/.github/scripts/prepare_JREs.sh
deleted file mode 100755
index ee713f81f..000000000
--- a/.github/scripts/prepare_JREs.sh
+++ /dev/null
@@ -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
diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml
index dee01e084..60bd86eec 100644
--- a/.github/workflows/backport.yml
+++ b/.github/workflows/backport.yml
@@ -16,6 +16,7 @@ jobs:
permissions:
contents: write # for korthout/backport-action to create branch
pull-requests: write # for korthout/backport-action to create PR to backport
+ actions: write # for korthout/backport-action to create PR with workflow changes
name: Backport Pull Request
if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name))
runs-on: ubuntu-latest
@@ -24,7 +25,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs
- uses: korthout/backport-action@v2.3.0
+ uses: korthout/backport-action@v2.5.0
with:
# Config README: https://github.com/korthout/backport-action#backport-action
pull_description: |-
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1159aad6c..999029bd2 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -21,8 +21,23 @@ on:
WINDOWS_CODESIGN_PASSWORD:
description: Password for signing Windows builds
required: false
- CACHIX_AUTH_TOKEN:
- description: Private token for authenticating against Cachix cache
+ APPLE_CODESIGN_CERT:
+ description: Certificate for signing macOS builds
+ required: false
+ APPLE_CODESIGN_PASSWORD:
+ description: Password for signing macOS builds
+ required: false
+ APPLE_CODESIGN_ID:
+ description: Certificate ID for signing macOS builds
+ required: false
+ APPLE_NOTARIZE_APPLE_ID:
+ description: Apple ID used for notarizing macOS builds
+ required: false
+ APPLE_NOTARIZE_TEAM_ID:
+ description: Team ID used for notarizing macOS builds
+ required: false
+ APPLE_NOTARIZE_PASSWORD:
+ description: Password used for notarizing macOS builds
required: false
GPG_PRIVATE_KEY:
description: Private key for AppImage signing
@@ -39,14 +54,17 @@ jobs:
include:
- os: ubuntu-20.04
qt_ver: 5
+ qt_host: linux
+ qt_arch: ""
+ qt_version: "5.12.8"
+ qt_modules: "qtnetworkauth"
- os: ubuntu-20.04
qt_ver: 6
qt_host: linux
qt_arch: ""
qt_version: "6.2.4"
- qt_modules: "qt5compat qtimageformats"
- qt_tools: ""
+ qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: windows-2022
name: "Windows-MinGW-w64"
@@ -60,10 +78,9 @@ jobs:
vcvars_arch: "amd64"
qt_ver: 6
qt_host: windows
- qt_arch: ''
- qt_version: '6.6.1'
- qt_modules: 'qt5compat qtimageformats'
- qt_tools: ''
+ qt_arch: ""
+ qt_version: "6.7.0"
+ qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: windows-2022
name: "Windows-MSVC-arm64"
@@ -72,20 +89,18 @@ jobs:
vcvars_arch: "amd64_arm64"
qt_ver: 6
qt_host: windows
- qt_arch: 'win64_msvc2019_arm64'
- qt_version: '6.6.1'
- qt_modules: 'qt5compat qtimageformats'
- qt_tools: ''
+ qt_arch: "win64_msvc2019_arm64"
+ qt_version: "6.7.0"
+ qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: macos-12
name: macOS
macosx_deployment_target: 11.0
qt_ver: 6
qt_host: mac
- qt_arch: ''
- qt_version: '6.6.1'
- qt_modules: 'qt5compat qtimageformats'
- qt_tools: ''
+ qt_arch: ""
+ qt_version: "6.7.0"
+ qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: macos-12
name: macOS-Legacy
@@ -93,8 +108,7 @@ jobs:
qt_ver: 5
qt_host: mac
qt_version: "5.15.2"
- qt_modules: ""
- qt_tools: ""
+ qt_modules: "qtnetworkauth"
runs-on: ${{ matrix.os }}
@@ -136,6 +150,7 @@ jobs:
quazip-qt6:p
ccache:p
qt6-5compat:p
+ qt6-networkauth:p
cmark:p
- name: Force newer ccache
@@ -145,13 +160,13 @@ jobs:
- name: Setup ccache
if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug'
- uses: hendrikmuhs/ccache-action@v1.2.11
+ uses: hendrikmuhs/ccache-action@v1.2.13
with:
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
- name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
- uses: actions/cache@v3.3.2
+ uses: actions/cache@v4.0.2
with:
path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
@@ -192,11 +207,6 @@ jobs:
brew update
brew install ninja extra-cmake-modules
- - name: Install Qt (Linux)
- if: runner.os == 'Linux' && matrix.qt_ver != 6
- run: |
- sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5
-
- name: Install host Qt (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
uses: jurplel/install-qt-action@v3
@@ -208,20 +218,18 @@ jobs:
target: "desktop"
arch: ""
modules: ${{ matrix.qt_modules }}
- tools: ${{ matrix.qt_tools }}
cache: ${{ inputs.is_qt_cached }}
cache-key-prefix: host-qt-arm64-windows
dir: ${{ github.workspace }}\HostQt
set-env: false
- - name: Install Qt (macOS, Linux, Qt 6 & Windows MSVC)
- if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS' || (runner.os == 'Windows' && matrix.msystem == '')
+ - name: Install Qt (macOS, Linux & Windows MSVC)
+ if: matrix.msystem == ''
uses: jurplel/install-qt-action@v3
with:
aqtversion: "==3.1.*"
py7zrversion: ">=0.20.2"
version: ${{ matrix.qt_version }}
- host: ${{ matrix.qt_host }}
target: "desktop"
arch: ${{ matrix.qt_arch }}
modules: ${{ matrix.qt_modules }}
@@ -244,7 +252,6 @@ jobs:
wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage"
- ${{ github.workspace }}/.github/scripts/prepare_JREs.sh
sudo apt install libopengl0
- name: Add QT_HOST_PATH var (Windows MSVC arm64)
@@ -336,6 +343,20 @@ jobs:
# PACKAGE BUILDS
##
+ - name: Fetch codesign certificate (macOS)
+ if: runner.os == 'macOS'
+ run: |
+ echo '${{ secrets.APPLE_CODESIGN_CERT }}' | base64 --decode > codesign.p12
+ if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then
+ security create-keychain -p '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain
+ security default-keychain -s build.keychain
+ security unlock-keychain -p '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain
+ security import codesign.p12 -k build.keychain -P '${{ secrets.APPLE_CODESIGN_PASSWORD }}' -T /usr/bin/codesign
+ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain
+ else
+ echo ":warning: Using ad-hoc code signing for macOS, as certificate was not present." >> $GITHUB_STEP_SUMMARY
+ fi
+
- name: Package (macOS)
if: runner.os == 'macOS'
run: |
@@ -343,9 +364,34 @@ jobs:
cd ${{ env.INSTALL_DIR }}
chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher"
- sudo codesign --sign - --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
+
+ if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then
+ APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}'
+ else
+ APPLE_CODESIGN_ID='-'
+ fi
+
+ sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
mv "PrismLauncher.app" "Prism Launcher.app"
- tar -czf ../PrismLauncher.tar.gz *
+
+ - name: Notarize (macOS)
+ if: runner.os == 'macOS'
+ run: |
+ cd ${{ env.INSTALL_DIR }}
+
+ if [ -n '${{ secrets.APPLE_NOTARIZE_PASSWORD }}' ]; then
+ ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip
+ xcrun notarytool submit ../PrismLauncher.zip \
+ --wait --progress \
+ --apple-id '${{ secrets.APPLE_NOTARIZE_APPLE_ID }}' \
+ --team-id '${{ secrets.APPLE_NOTARIZE_TEAM_ID }}' \
+ --password '${{ secrets.APPLE_NOTARIZE_PASSWORD }}'
+
+ xcrun stapler staple "Prism Launcher.app"
+ else
+ echo ":warning: Skipping notarization as credentials are not present." >> $GITHUB_STEP_SUMMARY
+ fi
+ ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip
- name: Make Sparkle signature (macOS)
if: matrix.name == 'macOS'
@@ -353,7 +399,7 @@ jobs:
if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then
brew install openssl@3
echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem
- signature=$(/usr/local/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.tar.gz -inkey ed25519-priv.pem | openssl base64 | tr -d \\n)
+ signature=$(/usr/local/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n)
rm ed25519-priv.pem
cat >> $GITHUB_STEP_SUMMARY << EOF
### Artifact Information :information_source:
@@ -379,12 +425,6 @@ jobs:
run: |
cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }}
- cd ${{ env.INSTALL_DIR }}
- if ("${{ matrix.qt_ver }}" -eq "5")
- {
- Copy-Item ${{ runner.workspace }}/Qt/Tools/OpenSSL/Win_x86/bin/libcrypto-1_1.dll -Destination libcrypto-1_1.dll
- Copy-Item ${{ runner.workspace }}/Qt/Tools/OpenSSL/Win_x86/bin/libssl-1_1.dll -Destination libssl-1_1.dll
- }
cd ${{ github.workspace }}
Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
@@ -437,26 +477,6 @@ jobs:
":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
}
- - name: Package (Linux)
- if: runner.os == 'Linux'
- run: |
- cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_DIR }}
- for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_DIR }}/manifest.txt
-
- cd ${{ env.INSTALL_DIR }}
- tar --owner root --group root -czf ../PrismLauncher.tar.gz *
-
- - name: Package (Linux, portable)
- if: runner.os == 'Linux'
- run: |
- cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }}
- cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
- for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
-
-
- cd ${{ env.INSTALL_PORTABLE_DIR }}
- tar -czf ../PrismLauncher-portable.tar.gz *
-
- name: Package AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5
shell: bash
@@ -472,13 +492,9 @@ jobs:
chmod +x linuxdeploy-*.AppImage
- mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-{8,17}-openjdk
+ mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib
mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
- cp -r ${{ github.workspace }}/JREs/jre8/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk
-
- cp -r ${{ github.workspace }}/JREs/jre17/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk
-
cp -r ${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
@@ -486,10 +502,6 @@ jobs:
cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
- LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server"
- LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64"
- LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk/lib/server"
- LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk/lib"
export LD_LIBRARY_PATH
chmod +x AppImageUpdate-x86_64.AppImage
@@ -511,6 +523,25 @@ jobs:
mv "PrismLauncher-Linux-x86_64.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage"
+ - name: Package (Linux, portable)
+ if: runner.os == 'Linux'
+ run: |
+ cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_PORTABLE_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -DINSTALL_BUNDLE=full -G Ninja
+ cmake --install ${{ env.BUILD_DIR }}
+ cmake --install ${{ env.BUILD_DIR }} --component portable
+
+ mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib
+ cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
+ cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
+ cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib
+ cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib
+ cp /usr/lib/x86_64-linux-gnu/libffi.so.7 ${{ env.INSTALL_PORTABLE_DIR }}/lib
+ mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib
+
+ for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
+ cd ${{ env.INSTALL_PORTABLE_DIR }}
+ tar -czf ../PrismLauncher-portable.tar.gz *
+
##
# UPLOAD BUILDS
##
@@ -520,7 +551,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
- path: PrismLauncher.tar.gz
+ path: PrismLauncher.zip
- name: Upload binary zip (Windows)
if: runner.os == 'Windows'
@@ -543,13 +574,6 @@ jobs:
name: PrismLauncher-${{ matrix.name }}-Setup-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher-Setup.exe
- - name: Upload binary tarball (Linux, Qt 5)
- if: runner.os == 'Linux' && matrix.qt_ver != 6
- uses: actions/upload-artifact@v4
- with:
- name: PrismLauncher-${{ runner.os }}-Qt5-${{ env.VERSION }}-${{ inputs.build_type }}
- path: PrismLauncher.tar.gz
-
- name: Upload binary tarball (Linux, portable, Qt 5)
if: runner.os == 'Linux' && matrix.qt_ver != 6
uses: actions/upload-artifact@v4
@@ -557,13 +581,6 @@ jobs:
name: PrismLauncher-${{ runner.os }}-Qt5-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher-portable.tar.gz
- - name: Upload binary tarball (Linux, Qt 6)
- if: runner.os == 'Linux' && matrix.qt_ver !=5
- uses: actions/upload-artifact@v4
- with:
- name: PrismLauncher-${{ runner.os }}-Qt6-${{ env.VERSION }}-${{ inputs.build_type }}
- path: PrismLauncher.tar.gz
-
- name: Upload binary tarball (Linux, portable, Qt 6)
if: runner.os == 'Linux' && matrix.qt_ver != 5
uses: actions/upload-artifact@v4
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index d40d7eb68..5255f865b 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -23,7 +23,7 @@ jobs:
run:
sudo apt-get -y update
- sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5
+ sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 libqt5networkauth5 libqt5networkauth5-dev
- name: Configure and Build
run: |
diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml
index 70fda60ed..9efafc8cc 100644
--- a/.github/workflows/trigger_builds.yml
+++ b/.github/workflows/trigger_builds.yml
@@ -32,6 +32,11 @@ jobs:
SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
- CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
+ APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }}
+ APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }}
+ APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }}
+ APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
+ APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
+ APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}
diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml
index ebf5f96e6..134281b2c 100644
--- a/.github/workflows/trigger_release.yml
+++ b/.github/workflows/trigger_release.yml
@@ -16,7 +16,12 @@ jobs:
SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
- CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
+ APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }}
+ APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }}
+ APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }}
+ APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
+ APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
+ APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}
@@ -41,13 +46,11 @@ jobs:
run: |
mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }}
mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz
- mv PrismLauncher-Linux-Qt6*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz
mv PrismLauncher-Linux-Qt5-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz
- mv PrismLauncher-Linux-Qt5*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt5-${{ env.VERSION }}.tar.gz
mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage
mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync
- mv PrismLauncher-macOS-Legacy*/PrismLauncher.tar.gz PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz
- mv PrismLauncher-macOS*/PrismLauncher.tar.gz PrismLauncher-macOS-${{ env.VERSION }}.tar.gz
+ mv PrismLauncher-macOS-Legacy*/PrismLauncher.zip PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip
+ mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip
tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}
@@ -79,7 +82,7 @@ jobs:
- name: Create release
id: create_release
- uses: softprops/action-gh-release@v1
+ uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ github.ref }}
@@ -87,11 +90,9 @@ jobs:
draft: true
prerelease: false
files: |
- PrismLauncher-Linux-Qt5-${{ env.VERSION }}.tar.gz
PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz
PrismLauncher-Linux-x86_64.AppImage
PrismLauncher-Linux-x86_64.AppImage.zsync
- PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz
PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz
PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip
PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip
@@ -102,6 +103,6 @@ jobs:
PrismLauncher-Windows-MSVC-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe
- PrismLauncher-macOS-${{ env.VERSION }}.tar.gz
- PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz
+ PrismLauncher-macOS-${{ env.VERSION }}.zip
+ PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip
PrismLauncher-${{ env.VERSION }}.tar.gz
diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml
index 538972968..855b105ea 100644
--- a/.github/workflows/update-flake.yml
+++ b/.github/workflows/update-flake.yml
@@ -17,9 +17,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- - uses: cachix/install-nix-action@7ac1ec25491415c381d9b62f0657c7a028df52a7 # v24
+ - uses: cachix/install-nix-action@8887e596b4ee1134dae06b98d573bd674693f47c # v26
- - uses: DeterminateSystems/update-flake-lock@v20
+ - uses: DeterminateSystems/update-flake-lock@v21
with:
commit-msg: "chore(nix): update lockfile"
pr-title: "chore(nix): update lockfile"
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e42186cb5..5c3987406 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -178,7 +178,7 @@ set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL th
set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help")
######## Set version numbers ########
-set(Launcher_VERSION_MAJOR 8)
+set(Launcher_VERSION_MAJOR 9)
set(Launcher_VERSION_MINOR 0)
set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
@@ -282,7 +282,7 @@ endif()
include(QtVersionlessBackport)
if(Launcher_QT_VERSION_MAJOR EQUAL 5)
set(QT_VERSION_MAJOR 5)
- find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml)
+ find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml NetworkAuth)
if(NOT Launcher_FORCE_BUNDLED_LIBS)
find_package(QuaZip-Qt5 1.3 QUIET)
@@ -296,7 +296,7 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 5)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE")
elseif(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_VERSION_MAJOR 6)
- find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat)
+ find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth)
list(APPEND Launcher_QT_LIBS Qt6::Core5Compat)
if(NOT Launcher_FORCE_BUNDLED_LIBS)
@@ -381,8 +381,8 @@ if(UNIX AND APPLE)
set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed")
set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed")
- set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.1.0/Sparkle-2.1.0.tar.xz" CACHE STRING "URL to Sparkle release archive")
- set(MACOSX_SPARKLE_SHA256 "bf6ac1caa9f8d321d5784859c88da874f28412f37fb327bc21b7b14c5d61ef94" CACHE STRING "SHA256 checksum for Sparkle release archive")
+ set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.5.2/Sparkle-2.5.2.tar.xz" CACHE STRING "URL to Sparkle release archive")
+ set(MACOSX_SPARKLE_SHA256 "572dd67ae398a466f19f343a449e1890bac1ef74885b4739f68f979a8a89884b" CACHE STRING "SHA256 checksum for Sparkle release archive")
set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle")
# directories to look for dependencies
@@ -417,7 +417,19 @@ elseif(UNIX)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR})
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}")
+
+ if (INSTALL_BUNDLE STREQUAL full)
+ set(PLUGIN_DEST_DIR "plugins")
+ set(BUNDLE_DEST_DIR ".")
+ set(RESOURCES_DEST_DIR ".")
+ # Apps to bundle
+ set(APPS "\${CMAKE_INSTALL_PREFIX}/bin/${Launcher_APP_BINARY_NAME}")
+
+ # directories to look for dependencies
+ set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+ endif()
+
if(Launcher_ManPage)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6")
endif()
@@ -504,15 +516,13 @@ else()
endif()
if(NOT cmark_FOUND)
message(STATUS "Using bundled cmark")
- set(CMARK_STATIC ON CACHE BOOL "Build static libcmark library" FORCE)
- set(CMARK_SHARED OFF CACHE BOOL "Build shared libcmark library" FORCE)
- set(CMARK_TESTS OFF CACHE BOOL "Build cmark tests and enable testing" FORCE)
+ set(BUILD_TESTING 0)
+ set(BUILD_SHARED_LIBS 0)
add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser
- add_library(cmark::cmark ALIAS cmark_static)
+ add_library(cmark::cmark ALIAS cmark)
else()
message(STATUS "Using system cmark")
endif()
-add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much
add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
if (NOT ghc_filesystem_FOUND)
diff --git a/COPYING.md b/COPYING.md
index f14e2958e..111587060 100644
--- a/COPYING.md
+++ b/COPYING.md
@@ -333,32 +333,6 @@
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-## O2 (Katabasis fork)
-
- Copyright (c) 2012, Akos Polster
- All rights reserved.
-
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions are met:
-
- * Redistributions of source code must retain the above copyright notice, this
- list of conditions and the following disclaimer.
-
- * Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
-
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
## Gamemode
Copyright (c) 2017-2022, Feral Interactive
diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in
index d36ac3e8f..c439efe25 100644
--- a/cmake/MacOSXBundleInfo.plist.in
+++ b/cmake/MacOSXBundleInfo.plist.in
@@ -6,6 +6,8 @@
A Minecraft mod wants to access your camera.
NSMicrophoneUsageDescription
A Minecraft mod wants to access your microphone.
+ NSDownloadsFolderUsageDescription
+ 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.
NSPrincipalClass
NSApplication
NSHighResolutionCapable
diff --git a/flake.lock b/flake.lock
index 9ed93f9fd..740d5c43e 100644
--- a/flake.lock
+++ b/flake.lock
@@ -18,14 +18,16 @@
},
"flake-parts": {
"inputs": {
- "nixpkgs-lib": "nixpkgs-lib"
+ "nixpkgs-lib": [
+ "nixpkgs"
+ ]
},
"locked": {
- "lastModified": 1704152458,
- "narHash": "sha256-DS+dGw7SKygIWf9w4eNBUZsK+4Ug27NwEWmn2tnbycg=",
+ "lastModified": 1714641030,
+ "narHash": "sha256-yzcRNDoyVP7+SCNX0wmuDju1NUCt8Dz9+lyUXEI0dbI=",
"owner": "hercules-ci",
"repo": "flake-parts",
- "rev": "88a2cd8166694ba0b6cb374700799cec53aef527",
+ "rev": "e5d10a24b66c3ea8f150e47dfdb0416ab7c3390e",
"type": "github"
},
"original": {
@@ -39,11 +41,11 @@
"systems": "systems"
},
"locked": {
- "lastModified": 1685518550,
- "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
+ "lastModified": 1710146030,
+ "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
+ "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
@@ -60,11 +62,11 @@
]
},
"locked": {
- "lastModified": 1660459072,
- "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
+ "lastModified": 1709087332,
+ "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
- "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
+ "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
@@ -89,28 +91,13 @@
"type": "github"
}
},
- "nix-filter": {
- "locked": {
- "lastModified": 1701697642,
- "narHash": "sha256-L217WytWZHSY8GW9Gx1A64OnNctbuDbfslaTEofXXRw=",
- "owner": "numtide",
- "repo": "nix-filter",
- "rev": "c843418ecfd0344ecb85844b082ff5675e02c443",
- "type": "github"
- },
- "original": {
- "owner": "numtide",
- "repo": "nix-filter",
- "type": "github"
- }
- },
"nixpkgs": {
"locked": {
- "lastModified": 1704161960,
- "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=",
+ "lastModified": 1715413075,
+ "narHash": "sha256-FCi3R1MeS5bVp0M0xTheveP6hhcCYfW/aghSTPebYL4=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "63143ac2c9186be6d9da6035fa22620018c85932",
+ "rev": "e4e7a43a9db7e22613accfeb1005cca1b2b1ee0d",
"type": "github"
},
"original": {
@@ -120,24 +107,6 @@
"type": "github"
}
},
- "nixpkgs-lib": {
- "locked": {
- "dir": "lib",
- "lastModified": 1703961334,
- "narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=",
- "owner": "NixOS",
- "repo": "nixpkgs",
- "rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9",
- "type": "github"
- },
- "original": {
- "dir": "lib",
- "owner": "NixOS",
- "ref": "nixos-unstable",
- "repo": "nixpkgs",
- "type": "github"
- }
- },
"pre-commit-hooks": {
"inputs": {
"flake-compat": [
@@ -153,11 +122,11 @@
]
},
"locked": {
- "lastModified": 1703939133,
- "narHash": "sha256-Gxe+mfOT6bL7wLC/tuT2F+V+Sb44jNr8YsJ3cyIl4Mo=",
+ "lastModified": 1714478972,
+ "narHash": "sha256-q//cgb52vv81uOuwz1LaXElp3XAe1TqrABXODAEF6Sk=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
- "rev": "9d3d7e18c6bc4473d7520200d4ddab12f8402d38",
+ "rev": "2849da033884f54822af194400f8dff435ada242",
"type": "github"
},
"original": {
@@ -171,7 +140,6 @@
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"libnbtplusplus": "libnbtplusplus",
- "nix-filter": "nix-filter",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
}
diff --git a/flake.nix b/flake.nix
index afb0ec63a..e16c76699 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1,15 +1,24 @@
{
description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of MultiMC)";
+ nixConfig = {
+ extra-substituters = ["https://cache.garnix.io"];
+ extra-trusted-public-keys = ["cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g="];
+ };
+
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
- flake-parts.url = "github:hercules-ci/flake-parts";
- nix-filter.url = "github:numtide/nix-filter";
+ flake-parts = {
+ url = "github:hercules-ci/flake-parts";
+ inputs.nixpkgs-lib.follows = "nixpkgs";
+ };
pre-commit-hooks = {
url = "github:cachix/pre-commit-hooks.nix";
- inputs.nixpkgs.follows = "nixpkgs";
- inputs.nixpkgs-stable.follows = "nixpkgs";
- inputs.flake-compat.follows = "flake-compat";
+ inputs = {
+ nixpkgs.follows = "nixpkgs";
+ nixpkgs-stable.follows = "nixpkgs";
+ flake-compat.follows = "flake-compat";
+ };
};
flake-compat = {
url = "github:edolstra/flake-compat";
diff --git a/flatpak/org.prismlauncher.PrismLauncher.yml b/flatpak/org.prismlauncher.PrismLauncher.yml
index c3ac132b1..b4c6e8143 100644
--- a/flatpak/org.prismlauncher.PrismLauncher.yml
+++ b/flatpak/org.prismlauncher.PrismLauncher.yml
@@ -3,6 +3,7 @@ runtime: org.kde.Platform
runtime-version: 5.15-23.08
sdk: org.kde.Sdk
sdk-extensions:
+ - org.freedesktop.Sdk.Extension.openjdk21
- org.freedesktop.Sdk.Extension.openjdk17
- org.freedesktop.Sdk.Extension.openjdk8
@@ -50,6 +51,8 @@ modules:
buildsystem: simple
build-commands:
- mkdir -p /app/jdk/
+ - /usr/lib/sdk/openjdk21/install.sh
+ - mv /app/jre /app/jdk/21
- /usr/lib/sdk/openjdk17/install.sh
- mv /app/jre /app/jdk/17
- /usr/lib/sdk/openjdk8/install.sh
diff --git a/flatpak/shared-modules b/flatpak/shared-modules
index 55a8e460c..f2b0c16a2 160000
--- a/flatpak/shared-modules
+++ b/flatpak/shared-modules
@@ -1 +1 @@
-Subproject commit 55a8e460c6343229597a13e973ba4855c27a1c4c
+Subproject commit f2b0c16a2a217a1822ce5a6538ba8f755ed1dd32
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index 7f51fe0e8..96a50f2ba 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -48,6 +48,7 @@
#include "pathmatcher/MultiMatcher.h"
#include "pathmatcher/SimplePrefixMatcher.h"
#include "settings/INIFile.h"
+#include "tools/GenericProfiler.h"
#include "ui/InstanceWindow.h"
#include "ui/MainWindow.h"
@@ -225,6 +226,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// Don't quit on hiding the last window
this->setQuitOnLastWindowClosed(false);
+ this->setQuitLockEnabled(false);
// Commandline parsing
QCommandLineParser parser;
@@ -308,7 +310,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
adjustedBy = "Persistent data path";
#ifndef Q_OS_MACOS
- if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
+ if (auto portableUserData = FS::PathCombine(m_rootPath, "UserData"); QDir(portableUserData).exists()) {
+ dataPath = portableUserData;
+ adjustedBy = "Portable user data path";
+ m_portable = true;
+ } else if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
dataPath = m_rootPath;
adjustedBy = "Portable data path";
m_portable = true;
@@ -639,10 +645,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("UseNativeGLFW", false);
m_settings->registerSetting("CustomGLFWPath", "");
- // Peformance related options
+ // Performance related options
m_settings->registerSetting("EnableFeralGamemode", false);
m_settings->registerSetting("EnableMangoHud", false);
m_settings->registerSetting("UseDiscreteGpu", false);
+ m_settings->registerSetting("UseZink", false);
// Game time
m_settings->registerSetting("ShowGameTime", true);
@@ -751,6 +758,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("ModrinthToken", "");
m_settings->registerSetting("UserAgentOverride", "");
+ // FTBApp instances
+ m_settings->registerSetting("FTBAppInstancesPath", "");
+
// Init page provider
{
m_globalSettingsProvider = std::make_shared(tr("Settings"));
@@ -870,6 +880,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// FIXME: what to do with these?
m_profilers.insert("jprofiler", std::shared_ptr(new JProfilerFactory()));
m_profilers.insert("jvisualvm", std::shared_ptr(new JVisualVMFactory()));
+ m_profilers.insert("generic", std::shared_ptr(new GenericProfilerFactory()));
for (auto profiler : m_profilers.values()) {
profiler->registerSettings(m_settings);
}
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 99acf8fc5..5c89f1fe0 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -126,7 +126,6 @@ set(NET_SOURCES
net/MetaCacheSink.h
net/Logging.h
net/Logging.cpp
- net/NetAction.h
net/NetJob.cpp
net/NetJob.h
net/NetUtils.h
@@ -210,28 +209,17 @@ set(MINECRAFT_SOURCES
minecraft/auth/AccountData.h
minecraft/auth/AccountList.cpp
minecraft/auth/AccountList.h
- minecraft/auth/AccountTask.cpp
- minecraft/auth/AccountTask.h
- minecraft/auth/AuthRequest.cpp
- minecraft/auth/AuthRequest.h
minecraft/auth/AuthSession.cpp
minecraft/auth/AuthSession.h
- minecraft/auth/AuthStep.cpp
minecraft/auth/AuthStep.h
minecraft/auth/MinecraftAccount.cpp
minecraft/auth/MinecraftAccount.h
minecraft/auth/Parsers.cpp
minecraft/auth/Parsers.h
- minecraft/auth/flows/AuthFlow.cpp
- minecraft/auth/flows/AuthFlow.h
- minecraft/auth/flows/MSA.cpp
- minecraft/auth/flows/MSA.h
- minecraft/auth/flows/Offline.cpp
- minecraft/auth/flows/Offline.h
+ minecraft/auth/AuthFlow.cpp
+ minecraft/auth/AuthFlow.h
- minecraft/auth/steps/OfflineStep.cpp
- minecraft/auth/steps/OfflineStep.h
minecraft/auth/steps/EntitlementsStep.cpp
minecraft/auth/steps/EntitlementsStep.h
minecraft/auth/steps/GetSkinStep.cpp
@@ -240,6 +228,8 @@ set(MINECRAFT_SOURCES
minecraft/auth/steps/LauncherLoginStep.h
minecraft/auth/steps/MinecraftProfileStep.cpp
minecraft/auth/steps/MinecraftProfileStep.h
+ minecraft/auth/steps/MSADeviceCodeStep.cpp
+ minecraft/auth/steps/MSADeviceCodeStep.h
minecraft/auth/steps/MSAStep.cpp
minecraft/auth/steps/MSAStep.h
minecraft/auth/steps/XboxAuthorizationStep.cpp
@@ -453,6 +443,8 @@ set(TOOLS_SOURCES
tools/JVisualVM.h
tools/MCEditTool.cpp
tools/MCEditTool.h
+ tools/GenericProfiler.cpp
+ tools/GenericProfiler.h
)
set(META_SOURCES
@@ -624,7 +616,6 @@ set(PRISMUPDATER_SOURCES
net/HttpMetaCache.h
net/Logging.h
net/Logging.cpp
- net/NetAction.h
net/NetRequest.cpp
net/NetRequest.h
net/NetJob.cpp
@@ -827,6 +818,8 @@ SET(LAUNCHER_SOURCES
ui/themes/DarkTheme.h
ui/themes/ITheme.cpp
ui/themes/ITheme.h
+ ui/themes/HintOverrideProxyStyle.cpp
+ ui/themes/HintOverrideProxyStyle.h
ui/themes/SystemTheme.cpp
ui/themes/SystemTheme.h
ui/themes/IconTheme.cpp
@@ -1239,7 +1232,6 @@ target_link_libraries(Launcher_logic
tomlplusplus::tomlplusplus
qdcss
BuildConfig
- Katabasis
Qt${QT_VERSION_MAJOR}::Widgets
ghcFilesystem::ghc_filesystem
)
@@ -1257,6 +1249,7 @@ target_link_libraries(Launcher_logic
Qt${QT_VERSION_MAJOR}::Concurrent
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Widgets
+ Qt${QT_VERSION_MAJOR}::NetworkAuth
${Launcher_QT_LIBS}
)
target_link_libraries(Launcher_logic
@@ -1327,7 +1320,6 @@ if(Launcher_BUILD_UPDATER)
Qt${QT_VERSION_MAJOR}::Network
${Launcher_QT_LIBS}
cmark::cmark
- Katabasis
)
add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp)
@@ -1492,7 +1484,6 @@ if(INSTALL_BUNDLE STREQUAL "full")
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
- PATTERN "*qopensslbackend*" EXCLUDE
PATTERN "*qcertonlybackend*" EXCLUDE
)
install(
@@ -1503,10 +1494,78 @@ if(INSTALL_BUNDLE STREQUAL "full")
REGEX "dd\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
- PATTERN "*qopensslbackend*" EXCLUDE
PATTERN "*qcertonlybackend*" EXCLUDE
)
endif()
+ # Wayland support
+ if(EXISTS "${QT_PLUGINS_DIR}/wayland-graphics-integration-client")
+ install(
+ DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-client"
+ CONFIGURATIONS Debug RelWithDebInfo ""
+ DESTINATION ${PLUGIN_DEST_DIR}
+ COMPONENT Runtime
+ )
+ install(
+ DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-client"
+ CONFIGURATIONS Release MinSizeRel
+ DESTINATION ${PLUGIN_DEST_DIR}
+ COMPONENT Runtime
+ REGEX "dd\\." EXCLUDE
+ REGEX "_debug\\." EXCLUDE
+ REGEX "\\.dSYM" EXCLUDE
+ )
+ endif()
+ if(EXISTS "${QT_PLUGINS_DIR}/wayland-graphics-integration-server")
+ install(
+ DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-server"
+ CONFIGURATIONS Debug RelWithDebInfo ""
+ DESTINATION ${PLUGIN_DEST_DIR}
+ COMPONENT Runtime
+ )
+ install(
+ DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-server"
+ CONFIGURATIONS Release MinSizeRel
+ DESTINATION ${PLUGIN_DEST_DIR}
+ COMPONENT Runtime
+ REGEX "dd\\." EXCLUDE
+ REGEX "_debug\\." EXCLUDE
+ REGEX "\\.dSYM" EXCLUDE
+ )
+ endif()
+ if(EXISTS "${QT_PLUGINS_DIR}/wayland-decoration-client")
+ install(
+ DIRECTORY "${QT_PLUGINS_DIR}/wayland-decoration-client"
+ CONFIGURATIONS Debug RelWithDebInfo ""
+ DESTINATION ${PLUGIN_DEST_DIR}
+ COMPONENT Runtime
+ )
+ install(
+ DIRECTORY "${QT_PLUGINS_DIR}/wayland-decoration-client"
+ CONFIGURATIONS Release MinSizeRel
+ DESTINATION ${PLUGIN_DEST_DIR}
+ COMPONENT Runtime
+ REGEX "dd\\." EXCLUDE
+ REGEX "_debug\\." EXCLUDE
+ REGEX "\\.dSYM" EXCLUDE
+ )
+ endif()
+ if(EXISTS "${QT_PLUGINS_DIR}/wayland-shell-integration")
+ install(
+ DIRECTORY "${QT_PLUGINS_DIR}/wayland-shell-integration"
+ CONFIGURATIONS Debug RelWithDebInfo ""
+ DESTINATION ${PLUGIN_DEST_DIR}
+ COMPONENT Runtime
+ )
+ install(
+ DIRECTORY "${QT_PLUGINS_DIR}/wayland-shell-integration"
+ CONFIGURATIONS Release MinSizeRel
+ DESTINATION ${PLUGIN_DEST_DIR}
+ COMPONENT Runtime
+ REGEX "dd\\." EXCLUDE
+ REGEX "_debug\\." EXCLUDE
+ REGEX "\\.dSYM" EXCLUDE
+ )
+ endif()
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake"
diff --git a/launcher/DesktopServices.cpp b/launcher/DesktopServices.cpp
index 17eb7c2df..841c1399c 100644
--- a/launcher/DesktopServices.cpp
+++ b/launcher/DesktopServices.cpp
@@ -37,140 +37,33 @@
#include
#include
#include
-
-/**
- * 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
-#include
-#include
-#include
-
-template
-bool IndirectOpen(T callable, qint64* pid_forked = nullptr)
-{
- auto pid = fork();
- if (pid_forked) {
- if (pid > 0)
- *pid_forked = pid;
- else
- *pid_forked = 0;
- }
- if (pid == -1) {
- qWarning() << "IndirectOpen failed to fork: " << errno;
- return false;
- }
- // child - do the stuff
- if (pid == 0) {
- // unset all this garbage so it doesn't get passed to the child process
- qunsetenv("LD_PRELOAD");
- qunsetenv("LD_LIBRARY_PATH");
- qunsetenv("LD_DEBUG");
- qunsetenv("QT_PLUGIN_PATH");
- qunsetenv("QT_FONTPATH");
-
- // open the URL
- auto status = callable();
-
- // detach from the parent process group.
- setsid();
-
- // die. now. do not clean up anything, it would just hang forever.
- _exit(status ? 0 : 1);
- } else {
- // parent - assume it worked.
- int status;
- while (waitpid(pid, &status, 0)) {
- if (WIFEXITED(status)) {
- return WEXITSTATUS(status) == 0;
- }
- if (WIFSIGNALED(status)) {
- return false;
- }
- }
- return true;
- }
-}
-#endif
+#include "FileSystem.h"
namespace DesktopServices {
-bool openDirectory(const QString& path, [[maybe_unused]] bool ensureExists)
+bool openPath(const QFileInfo& path, bool ensureFolderPathExists)
{
- qDebug() << "Opening directory" << path;
- QDir parentPath;
- QDir dir(path);
- if (ensureExists && !dir.exists()) {
- parentPath.mkpath(dir.absolutePath());
+ qDebug() << "Opening path" << path;
+ if (ensureFolderPathExists) {
+ FS::ensureFolderPathExists(path);
}
- auto f = [&]() { return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath())); };
-#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
- if (!isSandbox()) {
- return IndirectOpen(f);
- }
-#endif
- return f();
+ return openUrl(QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath()));
}
-bool openFile(const QString& path)
+bool openPath(const QString& path, bool ensureFolderPathExists)
{
- qDebug() << "Opening file" << path;
- auto f = [&]() { return QDesktopServices::openUrl(QUrl::fromLocalFile(path)); };
-#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
- if (!isSandbox()) {
- return IndirectOpen(f);
- } else {
- return f();
- }
-#else
- return f();
-#endif
-}
-
-bool openFile(const QString& application, const QString& path, const QString& workingDirectory, qint64* pid)
-{
- qDebug() << "Opening file" << path << "using" << application;
-#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
- // FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
- if (!isSandbox()) {
- return IndirectOpen([&]() { return QProcess::startDetached(application, QStringList() << path, workingDirectory); }, pid);
- } else {
- return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid);
- }
-#else
- return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid);
-#endif
+ return openPath(QFileInfo(path), ensureFolderPathExists);
}
bool run(const QString& application, const QStringList& args, const QString& workingDirectory, qint64* pid)
{
qDebug() << "Running" << application << "with args" << args.join(' ');
-#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
- if (!isSandbox()) {
- // FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
- return IndirectOpen([&]() { return QProcess::startDetached(application, args, workingDirectory); }, pid);
- } else {
- return QProcess::startDetached(application, args, workingDirectory, pid);
- }
-#else
return QProcess::startDetached(application, args, workingDirectory, pid);
-#endif
}
bool openUrl(const QUrl& url)
{
qDebug() << "Opening URL" << url.toString();
- auto f = [&]() { return QDesktopServices::openUrl(url); };
-#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
- if (!isSandbox()) {
- return IndirectOpen(f);
- } else {
- return f();
- }
-#else
- return f();
-#endif
+ return QDesktopServices::openUrl(url);
}
bool isFlatpak()
@@ -191,9 +84,4 @@ bool isSnap()
#endif
}
-bool isSandbox()
-{
- return isSnap() || isFlatpak();
-}
-
} // namespace DesktopServices
diff --git a/launcher/DesktopServices.h b/launcher/DesktopServices.h
index 151db5557..6c6208e82 100644
--- a/launcher/DesktopServices.h
+++ b/launcher/DesktopServices.h
@@ -3,31 +3,30 @@
#include
#include
+class QFileInfo;
+
/**
* This wraps around QDesktopServices and adds workarounds where needed
* Use this instead of QDesktopServices!
*/
namespace DesktopServices {
/**
- * Open a file in whatever application is applicable
+ * Open a path in whatever application is applicable.
+ * @param ensureFolderPathExists Make sure the path exists
*/
-bool openFile(const QString& path);
+bool openPath(const QFileInfo& path, bool ensureFolderPathExists = false);
/**
- * Open a file in the specified application
+ * Open a path in whatever application is applicable.
+ * @param ensureFolderPathExists Make sure the path exists
*/
-bool openFile(const QString& application, const QString& path, const QString& workingDirectory = QString(), qint64* pid = 0);
+bool openPath(const QString& path, bool ensureFolderPathExists = false);
/**
* Run an application
*/
bool run(const QString& application, const QStringList& args, const QString& workingDirectory = QString(), qint64* pid = 0);
-/**
- * Open a directory
- */
-bool openDirectory(const QString& path, bool ensureExists = false);
-
/**
* Open the URL, most likely in a browser. Maybe.
*/
@@ -42,9 +41,4 @@ bool isFlatpak();
* Determine whether the launcher is running in a Snap environment
*/
bool isSnap();
-
-/**
- * Determine whether the launcher is running in a sandboxed (Flatpak or Snap) environment
- */
-bool isSandbox();
} // namespace DesktopServices
diff --git a/launcher/Exception.h b/launcher/Exception.h
index ef1e4e0d8..55b40fdc8 100644
--- a/launcher/Exception.h
+++ b/launcher/Exception.h
@@ -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
+ *
+ * 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 .
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
#pragma once
@@ -8,12 +41,12 @@
class Exception : public std::exception {
public:
- Exception(const QString& message) : std::exception(), m_message(message) { qCritical() << "Exception:" << message; }
- Exception(const Exception& other) : std::exception(), m_message(other.cause()) {}
+ Exception(const QString& message) : std::exception(), m_message(message.toUtf8()) { qCritical() << "Exception:" << message; }
+ Exception(const Exception& other) : std::exception(), m_message(other.m_message) {}
virtual ~Exception() noexcept {}
- const char* what() const noexcept { return m_message.toLatin1().constData(); }
- QString cause() const { return m_message; }
+ const char* what() const noexcept { return m_message.constData(); }
+ QString cause() const { return QString::fromUtf8(m_message); }
private:
- QString m_message;
+ QByteArray m_message;
};
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
index a30d0ae0b..70704e1d3 100644
--- a/launcher/FileSystem.cpp
+++ b/launcher/FileSystem.cpp
@@ -272,15 +272,19 @@ bool ensureFilePathExists(QString filenamepath)
return success;
}
-bool ensureFolderPathExists(QString foldernamepath)
+bool ensureFolderPathExists(const QFileInfo folderPath)
{
- QFileInfo a(foldernamepath);
QDir dir;
- QString ensuredPath = a.filePath();
+ QString ensuredPath = folderPath.filePath();
bool success = dir.mkpath(ensuredPath);
return success;
}
+bool ensureFolderPathExists(const QString folderPathName)
+{
+ return ensureFolderPathExists(QFileInfo(folderPathName));
+}
+
bool copyFileAttributes(QString src, QString dst)
{
#ifdef Q_OS_WIN32
@@ -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)
{
- for (int i = 0; i < string.length(); i++) {
- if (badFilenameChars.contains(string[i])) {
+ for (int i = 0; i < string.length(); i++)
+ if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i)))
string[i] = replaceWith;
- }
- }
+
+ return string;
+}
+
+QString RemoveInvalidPathChars(QString 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;
}
@@ -1581,4 +1594,44 @@ uintmax_t hardLinkCount(const QString& path)
return count;
}
+#ifdef Q_OS_WIN
+// returns 8.3 file format from long path
+QString shortPathName(const QString& file)
+{
+ auto input = file.toStdWString();
+ std::wstring output;
+ long length = GetShortPathNameW(input.c_str(), NULL, 0);
+ if (length == 0)
+ return {};
+ // NOTE: this resizing might seem weird...
+ // when GetShortPathNameW fails, it returns length including null character
+ // when it succeeds, it returns length excluding null character
+ // See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx
+ output.resize(length);
+ if (GetShortPathNameW(input.c_str(), (LPWSTR)output.c_str(), length) == 0)
+ return {};
+ output.resize(length - 1);
+ QString ret = QString::fromStdWString(output);
+ return ret;
+}
+
+// if the string survives roundtrip through local 8bit encoding...
+bool fitsInLocal8bit(const QString& string)
+{
+ return string == QString::fromLocal8Bit(string.toLocal8Bit());
+}
+
+QString getPathNameInLocal8bit(const QString& file)
+{
+ if (!fitsInLocal8bit(file)) {
+ auto path = shortPathName(file);
+ if (!path.isEmpty()) {
+ return path;
+ }
+ // in case shortPathName fails just return the path as is
+ }
+ return file;
+}
+#endif
+
} // namespace FS
diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h
index 861cfa267..5496c3795 100644
--- a/launcher/FileSystem.h
+++ b/launcher/FileSystem.h
@@ -91,7 +91,13 @@ bool ensureFilePathExists(QString filenamepath);
* Creates all the folders in a path for the specified path
* last segment of the path is treated as a folder name and is created!
*/
-bool ensureFolderPathExists(QString filenamepath);
+bool ensureFolderPathExists(const QFileInfo folderPath);
+
+/**
+ * Creates all the folders in a path for the specified path
+ * last segment of the path is treated as a folder name and is created!
+ */
+bool ensureFolderPathExists(const QString folderPathName);
/**
* @brief Copies a directory and it's contents from src to dest
@@ -336,6 +342,8 @@ QString NormalizePath(QString path);
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-');
+QString RemoveInvalidPathChars(QString string, QChar replaceWith = '-');
+
QString DirNameFromString(QString string, QString inDir = ".");
/// Checks if the a given Path contains "!"
@@ -545,4 +553,8 @@ bool canLink(const QString& src, const QString& dst);
uintmax_t hardLinkCount(const QString& path);
+#ifdef Q_OS_WIN
+QString getPathNameInLocal8bit(const QString& file);
+#endif
+
} // namespace FS
diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp
index cdcd61ba6..52eb7d879 100644
--- a/launcher/InstanceCopyTask.cpp
+++ b/launcher/InstanceCopyTask.cpp
@@ -43,10 +43,10 @@ void InstanceCopyTask::executeTask()
QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
QString staging_mc_dir;
- if (mcDir.exists() && !dotMCDir.exists())
- staging_mc_dir = mcDir.filePath();
- else
+ if (dotMCDir.exists() && !mcDir.exists())
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"));
savesCopy.followSymlinks(true);
diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp
index 73dc17891..1499199ae 100644
--- a/launcher/InstanceCreationTask.cpp
+++ b/launcher/InstanceCreationTask.cpp
@@ -3,8 +3,6 @@
#include
#include
-InstanceCreationTask::InstanceCreationTask() = default;
-
void InstanceCreationTask::executeTask()
{
setAbortable(true);
diff --git a/launcher/InstanceCreationTask.h b/launcher/InstanceCreationTask.h
index 380fdf8a4..84fb2a145 100644
--- a/launcher/InstanceCreationTask.h
+++ b/launcher/InstanceCreationTask.h
@@ -6,7 +6,7 @@
class InstanceCreationTask : public InstanceTask {
Q_OBJECT
public:
- InstanceCreationTask();
+ InstanceCreationTask() = default;
virtual ~InstanceCreationTask() = default;
protected:
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
index 9f0cf7460..d4676f358 100644
--- a/launcher/InstanceImportTask.cpp
+++ b/launcher/InstanceImportTask.cpp
@@ -164,8 +164,8 @@ void InstanceImportTask::processZipPack()
} else if (technicFound) {
// process as Technic pack
qDebug() << "Technic:" << technicFound;
- extractDir.mkpath(".minecraft");
- extractDir.cd(".minecraft");
+ extractDir.mkpath("minecraft");
+ extractDir.cd("minecraft");
m_modpackType = ModpackType::Technic;
} else {
QStringList paths_to_ignore{ "overrides/" };
diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h
index a1cf2560b..28efd7ec5 100644
--- a/launcher/InstanceImportTask.h
+++ b/launcher/InstanceImportTask.h
@@ -47,9 +47,6 @@
#include
class QuaZip;
-namespace Flame {
-class FileResolvingTask;
-}
class InstanceImportTask : public InstanceTask {
Q_OBJECT
@@ -79,7 +76,6 @@ class InstanceImportTask : public InstanceTask {
private: /* data */
NetJob::Ptr m_filesNetJob;
- shared_qobject_ptr m_modIdResolver;
QUrl m_sourceUrl;
QString m_archivePath;
bool m_downloadRequired = false;
diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp
index c884a4f12..5e4abf020 100644
--- a/launcher/InstanceList.cpp
+++ b/launcher/InstanceList.cpp
@@ -38,6 +38,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -847,14 +848,16 @@ class InstanceStaging : public Task {
const unsigned maxBackoff = 16;
public:
- InstanceStaging(InstanceList* parent, InstanceTask* child, QString stagingPath, InstanceName const& instanceName, QString groupName)
- : m_parent(parent)
- , backoff(minBackoff, maxBackoff)
- , m_stagingPath(std::move(stagingPath))
- , m_instance_name(std::move(instanceName))
- , m_groupName(std::move(groupName))
+ InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObjectPtr settings)
+ : m_parent(parent), backoff(minBackoff, maxBackoff)
{
+ m_stagingPath = parent->getStagedInstancePath();
+
m_child.reset(child);
+
+ m_child->setStagingPath(m_stagingPath);
+ m_child->setParentSettings(std::move(settings));
+
connect(child, &Task::succeeded, this, &InstanceStaging::childSucceeded);
connect(child, &Task::failed, this, &InstanceStaging::childFailed);
connect(child, &Task::aborted, this, &InstanceStaging::childAborted);
@@ -866,7 +869,7 @@ class InstanceStaging : public Task {
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded);
}
- virtual ~InstanceStaging(){};
+ virtual ~InstanceStaging() {}
// FIXME/TODO: add ability to abort during instance commit retries
bool abort() override
@@ -881,14 +884,22 @@ class InstanceStaging : public Task {
bool canAbort() const override { return (m_child && m_child->canAbort()); }
protected:
- virtual void executeTask() override { m_child->start(); }
+ virtual void executeTask() override
+ {
+ if (m_stagingPath.isNull()) {
+ emitFailed(tr("Could not create staging folder"));
+ return;
+ }
+
+ m_child->start();
+ }
QStringList warnings() const override { return m_child->warnings(); }
private slots:
void childSucceeded()
{
unsigned sleepTime = backoff();
- if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, *m_child.get())) {
+ if (m_parent->commitStagedInstance(m_stagingPath, *m_child.get(), m_child->group(), *m_child.get())) {
emitSucceeded();
return;
}
@@ -897,7 +908,7 @@ class InstanceStaging : public Task {
emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something."));
return;
}
- qDebug() << "Failed to commit instance" << m_instance_name.name() << "Initiating backoff:" << sleepTime;
+ qDebug() << "Failed to commit instance" << m_child->name() << "Initiating backoff:" << sleepTime;
m_backoffTimer.start(sleepTime * 500);
}
void childFailed(const QString& reason)
@@ -906,7 +917,11 @@ class InstanceStaging : public Task {
emitFailed(reason);
}
- void childAborted() { emitAborted(); }
+ void childAborted()
+ {
+ m_parent->destroyStagingPath(m_stagingPath);
+ emitAborted();
+ }
private:
InstanceList* m_parent;
@@ -918,34 +933,35 @@ class InstanceStaging : public Task {
ExponentialSeries backoff;
QString m_stagingPath;
unique_qobject_ptr m_child;
- InstanceName m_instance_name;
- QString m_groupName;
QTimer m_backoffTimer;
};
Task* InstanceList::wrapInstanceTask(InstanceTask* task)
{
- auto stagingPath = getStagedInstancePath();
- task->setStagingPath(stagingPath);
- task->setParentSettings(m_globalSettings);
- return new InstanceStaging(this, task, stagingPath, *task, task->group());
+ return new InstanceStaging(this, task, m_globalSettings);
}
QString InstanceList::getStagedInstancePath()
{
- QString key = QUuid::createUuid().toString(QUuid::WithoutBraces);
- QString tempDir = ".LAUNCHER_TEMP/";
- QString relPath = FS::PathCombine(tempDir, key);
- QDir rootPath(m_instDir);
- auto path = FS::PathCombine(m_instDir, relPath);
- if (!rootPath.mkpath(relPath)) {
- return QString();
- }
+ const QString tempRoot = FS::PathCombine(m_instDir, ".tmp");
+
+ QString result;
+ int tries = 0;
+
+ do {
+ if (++tries > 256)
+ return {};
+
+ const QString key = QUuid::createUuid().toString(QUuid::Id128).left(6);
+ result = FS::PathCombine(tempRoot, key);
+ } while (QFileInfo::exists(result));
+
+ if (!QDir::current().mkpath(result))
+ return {};
#ifdef Q_OS_WIN32
- auto tempPath = FS::PathCombine(m_instDir, tempDir);
- SetFileAttributesA(tempPath.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
+ SetFileAttributesA(tempRoot.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
#endif
- return path;
+ return result;
}
bool InstanceList::commitStagedInstance(const QString& path,
diff --git a/launcher/InstanceTask.cpp b/launcher/InstanceTask.cpp
index 5b2398268..53476897c 100644
--- a/launcher/InstanceTask.cpp
+++ b/launcher/InstanceTask.cpp
@@ -2,6 +2,8 @@
#include "ui/dialogs/CustomMessageBox.h"
+#include
+
InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name)
{
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 "
"updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).")
.arg(original_version_name),
- QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort);
- info->setButtonText(QMessageBox::Ok, QObject::tr("Update existing instance"));
- info->setButtonText(QMessageBox::Abort, QObject::tr("Create new instance"));
- info->setButtonText(QMessageBox::Reset, QObject::tr("Cancel"));
+ QMessageBox::Information, QMessageBox::Cancel);
+ QAbstractButton* update = info->addButton(QObject::tr("Update existing instance"), QMessageBox::AcceptRole);
+ QAbstractButton* skip = info->addButton(QObject::tr("Create new instance"), QMessageBox::ResetRole);
info->exec();
- if (info->clickedButton() == info->button(QMessageBox::Ok))
+ if (info->clickedButton() == update)
return ShouldUpdate::Update;
- if (info->clickedButton() == info->button(QMessageBox::Abort))
+ if (info->clickedButton() == skip)
return ShouldUpdate::SkipUpdating;
return ShouldUpdate::Cancel;
}
diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp
index 8cef60e85..824564a2d 100644
--- a/launcher/LaunchController.cpp
+++ b/launcher/LaunchController.cpp
@@ -42,7 +42,6 @@
#include "ui/InstanceWindow.h"
#include "ui/MainWindow.h"
#include "ui/dialogs/CustomMessageBox.h"
-#include "ui/dialogs/EditAccountDialog.h"
#include "ui/dialogs/ProfileSelectDialog.h"
#include "ui/dialogs/ProfileSetupDialog.h"
#include "ui/dialogs/ProgressDialog.h"
@@ -58,7 +57,6 @@
#include "BuildConfig.h"
#include "JavaCommon.h"
#include "launch/steps/TextPrint.h"
-#include "minecraft/auth/AccountTask.h"
#include "tasks/Task.h"
LaunchController::LaunchController(QObject* parent) : Task(parent) {}
@@ -195,6 +193,12 @@ void LaunchController::login()
bool tryagain = true;
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) {
if (tries > 0 && tries % 3 == 0) {
auto result =
@@ -279,12 +283,6 @@ void LaunchController::login()
progDialog.execWithTask(task.get());
continue;
}
- // FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that
- /*
- case AccountState::Queued: {
- return;
- }
- */
case AccountState::Expired: {
auto errorString = tr("The account has expired and needs to be logged into manually again.");
QMessageBox::warning(m_parentWidget, tr("Account refresh failed"), errorString, QMessageBox::StandardButton::Ok,
diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp
index ce2573329..9a5ae7a9d 100644
--- a/launcher/MMCZip.cpp
+++ b/launcher/MMCZip.cpp
@@ -119,6 +119,7 @@ bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool follow
bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks)
{
QuaZip zip(fileCompressed);
+ zip.setUtf8Enabled(true);
QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
if (!zip.open(QuaZip::mdCreate)) {
QFile::remove(fileCompressed);
@@ -141,6 +142,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files,
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods)
{
QuaZip zipOut(targetJarPath);
+ zipOut.setUtf8Enabled(true);
if (!zipOut.open(QuaZip::mdCreate)) {
QFile::remove(targetJarPath);
qCritical() << "Failed to open the minecraft.jar for modding";
@@ -286,10 +288,13 @@ std::optional extractSubDir(QuaZip* zip, const QString& subdir, con
do {
QString file_name = zip->getCurrentFileName();
+#ifdef Q_OS_WIN
+ file_name = FS::RemoveInvalidPathChars(file_name);
+#endif
if (!file_name.startsWith(subdir))
continue;
- auto relative_file_name = QDir::fromNativeSeparators(file_name.remove(0, subdir.size()));
+ auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size()));
auto original_name = relative_file_name;
// Fix subdirs/files ending with a / getting transformed into absolute paths
@@ -463,7 +468,7 @@ auto ExportToZipTask::exportZip() -> ZipResult
auto absolute = file.absoluteFilePath();
auto relative = m_dir.relativeFilePath(absolute);
- setStatus("Compresing: " + relative);
+ setStatus("Compressing: " + relative);
setProgress(m_progress + 1, m_progressTotal);
if (m_follow_symlinks) {
if (file.isSymLink())
diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h
index 93692d0d2..b28eb195c 100644
--- a/launcher/MMCZip.h
+++ b/launcher/MMCZip.h
@@ -154,7 +154,12 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q
#if defined(LAUNCHER_APPLICATION)
class ExportToZipTask : public Task {
public:
- ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
+ ExportToZipTask(QString outputPath,
+ QDir dir,
+ QFileInfoList files,
+ QString destinationPrefix = "",
+ bool followSymlinks = false,
+ bool utf8Enabled = false)
: m_output_path(outputPath)
, m_output(outputPath)
, m_dir(dir)
@@ -163,9 +168,15 @@ class ExportToZipTask : public Task {
, m_follow_symlinks(followSymlinks)
{
setAbortable(true);
+ m_output.setUtf8Enabled(utf8Enabled);
};
- ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
- : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks){};
+ ExportToZipTask(QString outputPath,
+ QString dir,
+ QFileInfoList files,
+ QString destinationPrefix = "",
+ bool followSymlinks = false,
+ bool utf8Enabled = false)
+ : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled){};
virtual ~ExportToZipTask() = default;
diff --git a/launcher/install_prereqs.cmake.in b/launcher/install_prereqs.cmake.in
index e4408d161..acbce9650 100644
--- a/launcher/install_prereqs.cmake.in
+++ b/launcher/install_prereqs.cmake.in
@@ -1,5 +1,4 @@
set(CMAKE_MODULE_PATH "@CMAKE_MODULE_PATH@")
-
file(GLOB_RECURSE QTPLUGINS "${CMAKE_INSTALL_PREFIX}/@PLUGIN_DEST_DIR@/*@CMAKE_SHARED_LIBRARY_SUFFIX@")
function(gp_resolved_file_type_override resolved_file type_var)
if(resolved_file MATCHES "^/(usr/)?lib/libQt")
diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp
index 20caba189..fc8da55c2 100644
--- a/launcher/java/JavaChecker.cpp
+++ b/launcher/java/JavaChecker.cpp
@@ -55,6 +55,9 @@ void JavaChecker::performCheck()
qDebug() << "Java checker library could not be found. Please check your installation.";
return;
}
+#ifdef Q_OS_WIN
+ checkerJar = FS::getPathNameInLocal8bit(checkerJar);
+#endif
QStringList args;
diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp
index 074bf54df..e767eff89 100644
--- a/launcher/java/JavaUtils.cpp
+++ b/launcher/java/JavaUtils.cpp
@@ -362,6 +362,12 @@ QList JavaUtils::FindJavaPaths()
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java");
}
+
+ auto home = qEnvironmentVariable("HOME");
+
+ // javas downloaded by sdkman
+ javas.append(FS::PathCombine(home, ".sdkman/candidates/java"));
+
javas.append(getMinecraftJavaBundle());
javas = addJavasFromEnv(javas);
javas.removeDuplicates();
@@ -413,6 +419,8 @@ QList JavaUtils::FindJavaPaths()
scanJavaDirs(FS::PathCombine(home, ".jdks"));
// javas downloaded by sdkman
scanJavaDirs(FS::PathCombine(home, ".sdkman/candidates/java"));
+ // javas downloaded by gradle (toolchains)
+ scanJavaDirs(FS::PathCombine(home, ".gradle/jdks"));
javas.append(getMinecraftJavaBundle());
javas = addJavasFromEnv(javas);
@@ -439,26 +447,25 @@ QString JavaUtils::getJavaCheckPath()
QStringList getMinecraftJavaBundle()
{
- QString partialPath;
QString executable = "java";
QStringList processpaths;
#if defined(Q_OS_OSX)
- partialPath = FS::PathCombine(QDir::homePath(), "Library/Application Support");
+ processpaths << FS::PathCombine(QDir::homePath(), FS::PathCombine("Library", "Application Support", "minecraft", "runtime"));
#elif defined(Q_OS_WIN32)
- partialPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", "");
executable += "w.exe";
+ auto appDataPath = QProcessEnvironment::systemEnvironment().value("APPDATA", "");
+ processpaths << FS::PathCombine(QFileInfo(appDataPath).absoluteFilePath(), ".minecraft", "runtime");
+
// add the microsoft store version of the launcher to the search. the current path is:
// C:\Users\USERNAME\AppData\Local\Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\runtime
+ auto localAppDataPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", "");
auto minecraftMSStorePath =
- FS::PathCombine(QFileInfo(partialPath).absolutePath(), "Local", "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe");
- minecraftMSStorePath = FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime");
- processpaths << minecraftMSStorePath;
+ FS::PathCombine(QFileInfo(localAppDataPath).absoluteFilePath(), "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe");
+ processpaths << FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime");
#else
- partialPath = QDir::homePath();
+ processpaths << FS::PathCombine(QDir::homePath(), ".minecraft", "runtime");
#endif
- auto minecraftDataPath = FS::PathCombine(partialPath, ".minecraft", "runtime");
- processpaths << minecraftDataPath;
QStringList javas;
while (!processpaths.isEmpty()) {
diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp
index 48e150d16..6bbe0bb2c 100644
--- a/launcher/minecraft/AssetsUtils.cpp
+++ b/launcher/minecraft/AssetsUtils.cpp
@@ -51,6 +51,7 @@
#include "net/Download.h"
#include "Application.h"
+#include "net/NetRequest.h"
namespace {
QSet collectPathsFromDir(QString dirPath)
@@ -276,7 +277,7 @@ bool reconstructAssets(QString assetsId, QString resourcesFolder)
} // namespace AssetsUtils
-NetAction::Ptr AssetObject::getDownloadAction()
+Net::NetRequest::Ptr AssetObject::getDownloadAction()
{
QFileInfo objectFile(getLocalPath());
if ((!objectFile.isFile()) || (objectFile.size() != size)) {
diff --git a/launcher/minecraft/AssetsUtils.h b/launcher/minecraft/AssetsUtils.h
index 87956e57a..ea3613bd0 100644
--- a/launcher/minecraft/AssetsUtils.h
+++ b/launcher/minecraft/AssetsUtils.h
@@ -17,14 +17,14 @@
#include
#include
-#include "net/NetAction.h"
#include "net/NetJob.h"
+#include "net/NetRequest.h"
struct AssetObject {
QString getRelPath();
QUrl getUrl();
QString getLocalPath();
- NetAction::Ptr getDownloadAction();
+ Net::NetRequest::Ptr getDownloadAction();
QString hash;
qint64 size;
diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp
index 0e8ddf03d..2c3f2035f 100644
--- a/launcher/minecraft/Library.cpp
+++ b/launcher/minecraft/Library.cpp
@@ -35,6 +35,7 @@
#include "Library.h"
#include "MinecraftInstance.h"
+#include "net/NetRequest.h"
#include
#include
@@ -74,12 +75,12 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
}
}
-QList Library::getDownloads(const RuntimeContext& runtimeContext,
- class HttpMetaCache* cache,
- QStringList& failedLocalFiles,
- const QString& overridePath) const
+QList Library::getDownloads(const RuntimeContext& runtimeContext,
+ class HttpMetaCache* cache,
+ QStringList& failedLocalFiles,
+ const QString& overridePath) const
{
- QList out;
+ QList out;
bool stale = isAlwaysStale();
bool local = isLocal();
diff --git a/launcher/minecraft/Library.h b/launcher/minecraft/Library.h
index adb89c4c6..d3019e814 100644
--- a/launcher/minecraft/Library.h
+++ b/launcher/minecraft/Library.h
@@ -34,7 +34,6 @@
*/
#pragma once
-#include
#include
#include
#include
@@ -48,6 +47,7 @@
#include "MojangDownloadInfo.h"
#include "Rule.h"
#include "RuntimeContext.h"
+#include "net/NetRequest.h"
class Library;
class MinecraftInstance;
@@ -144,10 +144,10 @@ class Library {
bool isForge() const;
// Get a list of downloads for this library
- QList getDownloads(const RuntimeContext& runtimeContext,
- class HttpMetaCache* cache,
- QStringList& failedLocalFiles,
- const QString& overridePath) const;
+ QList getDownloads(const RuntimeContext& runtimeContext,
+ class HttpMetaCache* cache,
+ QStringList& failedLocalFiles,
+ const QString& overridePath) const;
QString getCompatibleNative(const RuntimeContext& runtimeContext) const;
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index 4229f73eb..1de822b7f 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -173,11 +173,12 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride);
m_settings->registerOverride(global_settings->getSetting("CustomGLFWPath"), nativeLibraryWorkaroundsOverride);
- // Peformance related options
+ // Performance related options
auto performanceOverride = m_settings->registerSetting("OverridePerformance", false);
m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride);
m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride);
m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride);
+ m_settings->registerOverride(global_settings->getSetting("UseZink"), performanceOverride);
// Miscellaneous
auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false);
@@ -292,10 +293,10 @@ QString MinecraftInstance::gameRoot() const
QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft"));
QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft"));
- if (mcDir.exists() && !dotMCDir.exists())
- return mcDir.filePath();
- else
+ if (dotMCDir.exists() && !mcDir.exists())
return dotMCDir.filePath();
+ else
+ return mcDir.filePath();
}
QString MinecraftInstance::binRoot() const
@@ -594,9 +595,6 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
QStringList preloadList;
if (auto value = env.value("LD_PRELOAD"); !value.isEmpty())
preloadList = value.split(QLatin1String(":"));
- QStringList libPaths;
- if (auto value = env.value("LD_LIBRARY_PATH"); !value.isEmpty())
- libPaths = value.split(QLatin1String(":"));
auto mangoHudLibString = MangoHud::getLibraryString();
if (!mangoHudLibString.isEmpty()) {
@@ -604,18 +602,16 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
QString libPath = mangoHudLib.absolutePath();
auto appendLib = [libPath, &preloadList](QString fileName) {
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
appendLib("libMangoHud_dlsym.so");
appendLib("libMangoHud_opengl.so");
appendLib(mangoHudLib.fileName());
- libPaths << libPath;
}
env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":")));
- env.insert("LD_LIBRARY_PATH", libPaths.join(QLatin1String(":")));
env.insert("MANGOHUD", "1");
}
@@ -627,6 +623,13 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only");
env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia");
}
+
+ if (settings()->get("UseZink").toBool()) {
+ // taken from https://wiki.archlinux.org/title/OpenGL#OpenGL_over_Vulkan_(Zink)
+ env.insert("__GLX_VENDOR_LIBRARY_NAME", "mesa");
+ env.insert("MESA_LOADER_DRIVER_OVERRIDE", "zink");
+ env.insert("GALLIUM_DRIVER", "zink");
+ }
#endif
return env;
}
@@ -662,8 +665,12 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine
}
if (serverToJoin && !serverToJoin->address.isEmpty()) {
- args_pattern += " --server " + serverToJoin->address;
- args_pattern += " --port " + QString::number(serverToJoin->port);
+ if (profile->hasTrait("feature:is_quick_play_multiplayer")) {
+ args_pattern += " --quickPlayMultiplayer " + serverToJoin->address + ':' + QString::number(serverToJoin->port);
+ } else {
+ args_pattern += " --server " + serverToJoin->address;
+ args_pattern += " --port " + QString::number(serverToJoin->port);
+ }
}
QMap token_mapping;
diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp
index e1f1e9b1e..fd2082035 100644
--- a/launcher/minecraft/auth/AccountData.cpp
+++ b/launcher/minecraft/auth/AccountData.cpp
@@ -42,7 +42,7 @@
#include
namespace {
-void tokenToJSONV3(QJsonObject& parent, Katabasis::Token t, const char* tokenName)
+void tokenToJSONV3(QJsonObject& parent, Token t, const char* tokenName)
{
if (!t.persistent) {
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();
if (tokenObject.isEmpty()) {
return out;
@@ -94,7 +94,7 @@ Katabasis::Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenNam
auto token = tokenObject.value("token");
if (token.isString()) {
out.token = token.toString();
- out.validity = Katabasis::Validity::Assumed;
+ out.validity = Validity::Assumed;
}
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;
}
void entitlementToJSONV3(QJsonObject& parent, MinecraftEntitlement p)
{
- if (p.validity == Katabasis::Validity::None) {
+ if (p.validity == Validity::None) {
return;
}
QJsonObject out;
@@ -271,7 +271,7 @@ bool entitlementFromJSONV3(const QJsonObject& parent, MinecraftEntitlement& out)
}
out.canPlayMinecraft = canPlayMinecraftV.toBool(false);
out.ownsMinecraft = ownsMinecraftV.toBool(false);
- out.validity = Katabasis::Validity::Assumed;
+ out.validity = Validity::Assumed;
}
return true;
}
@@ -313,10 +313,10 @@ bool AccountData::resumeStateFromV3(QJsonObject data)
minecraftProfile = profileFromJSONV3(data, "profile");
if (!entitlementFromJSONV3(data, minecraftEntitlement)) {
- if (minecraftProfile.validity != Katabasis::Validity::None) {
+ if (minecraftProfile.validity != Validity::None) {
minecraftEntitlement.canPlayMinecraft = true;
minecraftEntitlement.ownsMinecraft = true;
- minecraftEntitlement.validity = Katabasis::Validity::Assumed;
+ minecraftEntitlement.validity = Validity::Assumed;
}
}
diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h
index bac77e17f..1ada4e38a 100644
--- a/launcher/minecraft/auth/AccountData.h
+++ b/launcher/minecraft/auth/AccountData.h
@@ -34,12 +34,29 @@
*/
#pragma once
-#include
#include
#include
#include
#include
+#include
+#include
+#include
+#include
+
+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 {
QString id;
QString url;
@@ -59,7 +76,7 @@ struct Cape {
struct MinecraftEntitlement {
bool ownsMinecraft = false;
bool canPlayMinecraft = false;
- Katabasis::Validity validity = Katabasis::Validity::None;
+ Validity validity = Validity::None;
};
struct MinecraftProfile {
@@ -68,7 +85,7 @@ struct MinecraftProfile {
Skin skin;
QString currentCape;
QMap capes;
- Katabasis::Validity validity = Katabasis::Validity::None;
+ Validity validity = Validity::None;
};
enum class AccountType { MSA, Offline };
@@ -93,15 +110,15 @@ struct AccountData {
AccountType type = AccountType::MSA;
QString msaClientID;
- Katabasis::Token msaToken;
- Katabasis::Token userToken;
- Katabasis::Token xboxApiToken;
- Katabasis::Token mojangservicesToken;
+ Token msaToken;
+ Token userToken;
+ Token xboxApiToken;
+ Token mojangservicesToken;
- Katabasis::Token yggdrasilToken;
+ Token yggdrasilToken;
MinecraftProfile minecraftProfile;
MinecraftEntitlement minecraftEntitlement;
- Katabasis::Validity validity_ = Katabasis::Validity::None;
+ Validity validity_ = Validity::None;
// runtime only information (not saved with the account)
QString internalId;
diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp
index c3c09003c..d276d4c41 100644
--- a/launcher/minecraft/auth/AccountList.cpp
+++ b/launcher/minecraft/auth/AccountList.cpp
@@ -35,7 +35,7 @@
#include "AccountList.h"
#include "AccountData.h"
-#include "AccountTask.h"
+#include "tasks/Task.h"
#include
#include
@@ -52,8 +52,6 @@
#include
#include
-#include
-
enum AccountListVersion { MojangMSA = 3 };
AccountList::AccountList(QObject* parent) : QAbstractListModel(parent)
@@ -641,8 +639,8 @@ void AccountList::tryNext()
if (account->internalId() == accountId) {
m_currentTask = account->refresh();
if (m_currentTask) {
- connect(m_currentTask.get(), &AccountTask::succeeded, this, &AccountList::authSucceeded);
- connect(m_currentTask.get(), &AccountTask::failed, this, &AccountList::authFailed);
+ connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded);
+ connect(m_currentTask.get(), &Task::failed, this, &AccountList::authFailed);
m_currentTask->start();
qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID "
<< accountId;
diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h
index 039730739..d3be6740e 100644
--- a/launcher/minecraft/auth/AccountList.h
+++ b/launcher/minecraft/auth/AccountList.h
@@ -36,6 +36,7 @@
#pragma once
#include "MinecraftAccount.h"
+#include "minecraft/auth/AuthFlow.h"
#include
#include
@@ -144,7 +145,7 @@ class AccountList : public QAbstractListModel {
QList m_refreshQueue;
QTimer* m_refreshTimer;
QTimer* m_nextTimer;
- shared_qobject_ptr m_currentTask;
+ shared_qobject_ptr m_currentTask;
/*!
* Called whenever the list changes.
diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp
deleted file mode 100644
index 4c3d6ee19..000000000
--- a/launcher/minecraft/auth/AccountTask.cpp
+++ /dev/null
@@ -1,134 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * Prism Launcher - Minecraft Launcher
- * Copyright (C) 2022 Sefa Eyeoglu
- *
- * 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 .
- *
- * 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
-#include
-#include
-#include
-#include
-#include
-
-#include
-
-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;
- }
- }
-}
diff --git a/launcher/minecraft/auth/AccountTask.h b/launcher/minecraft/auth/AccountTask.h
deleted file mode 100644
index 82332c0b9..000000000
--- a/launcher/minecraft/auth/AccountTask.h
+++ /dev/null
@@ -1,92 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * Prism Launcher - Minecraft Launcher
- * Copyright (C) 2022 Sefa Eyeoglu
- *
- * 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 .
- *
- * 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
-
-#include
-#include
-#include
-#include
-
-#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;
-};
diff --git a/launcher/minecraft/auth/AuthFlow.cpp b/launcher/minecraft/auth/AuthFlow.cpp
new file mode 100644
index 000000000..5648fe9f6
--- /dev/null
+++ b/launcher/minecraft/auth/AuthFlow.cpp
@@ -0,0 +1,146 @@
+#include
+#include
+#include
+#include
+
+#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
+
+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(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(m_data, action == Action::Refresh);
+ connect(oauthStep.get(), &MSAStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowser);
+ m_steps.append(oauthStep);
+ }
+ m_steps.append(makeShared(m_data));
+ m_steps.append(makeShared(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
+ m_steps.append(
+ makeShared(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
+ m_steps.append(makeShared(m_data));
+ m_steps.append(makeShared(m_data));
+ m_steps.append(makeShared(m_data));
+ m_steps.append(makeShared(m_data));
+ m_steps.append(makeShared(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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/launcher/minecraft/auth/AuthFlow.h b/launcher/minecraft/auth/AuthFlow.h
new file mode 100644
index 000000000..d99deec3c
--- /dev/null
+++ b/launcher/minecraft/auth/AuthFlow.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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 m_steps;
+ AuthStep::Ptr m_currentStep;
+ AccountData* m_data = nullptr;
+};
diff --git a/launcher/minecraft/auth/AuthRequest.cpp b/launcher/minecraft/auth/AuthRequest.cpp
deleted file mode 100644
index 189978cc0..000000000
--- a/launcher/minecraft/auth/AuthRequest.cpp
+++ /dev/null
@@ -1,175 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * Prism Launcher - Minecraft Launcher
- * Copyright (C) 2022 Sefa Eyeoglu
- *
- * 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 .
- *
- * 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
-
-#include
-#include
-#include
-#include
-
-#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::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::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(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(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 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(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 headers = reply_->rawHeaderPairs();
- emit finished(error_, data, headers);
-}
diff --git a/launcher/minecraft/auth/AuthRequest.h b/launcher/minecraft/auth/AuthRequest.h
deleted file mode 100644
index 84d2a7d68..000000000
--- a/launcher/minecraft/auth/AuthRequest.h
+++ /dev/null
@@ -1,67 +0,0 @@
-#pragma once
-#include
-#include
-#include
-#include
-#include
-#include
-
-#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 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 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_;
-};
diff --git a/launcher/minecraft/auth/AuthStep.cpp b/launcher/minecraft/auth/AuthStep.cpp
deleted file mode 100644
index 6240cc549..000000000
--- a/launcher/minecraft/auth/AuthStep.cpp
+++ /dev/null
@@ -1,5 +0,0 @@
-#include "AuthStep.h"
-
-AuthStep::AuthStep(AccountData* data) : QObject(nullptr), m_data(data) {}
-
-AuthStep::~AuthStep() noexcept = default;
diff --git a/launcher/minecraft/auth/AuthStep.h b/launcher/minecraft/auth/AuthStep.h
index becd9b0c5..4d2cf69c1 100644
--- a/launcher/minecraft/auth/AuthStep.h
+++ b/launcher/minecraft/auth/AuthStep.h
@@ -3,30 +3,40 @@
#include
#include
-#include "AccountTask.h"
#include "QObjectPtr.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 {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr;
- public:
- explicit AuthStep(AccountData* data);
- virtual ~AuthStep() noexcept;
+ explicit AuthStep(AccountData* data) : QObject(nullptr), m_data(data){};
+ virtual ~AuthStep() noexcept = default;
virtual QString describe() = 0;
public slots:
virtual void perform() = 0;
- virtual void rehydrate() = 0;
signals:
void finished(AccountTaskState resultingState, QString message);
- void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
- void hideVerificationUriAndCode();
protected:
AccountData* m_data;
diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp
index 2ec6f2363..eabe9d98d 100644
--- a/launcher/minecraft/auth/MinecraftAccount.cpp
+++ b/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -50,9 +50,8 @@
#include
-#include "flows/MSA.h"
-#include "flows/Offline.h"
#include "minecraft/auth/AccountData.h"
+#include "minecraft/auth/AuthFlow.h"
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent)
{
@@ -80,7 +79,7 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username)
auto account = makeShared();
account->data.type = AccountType::Offline;
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.extra["userName"] = username;
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.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftProfile.name = username;
- account->data.minecraftProfile.validity = Katabasis::Validity::Certain;
+ account->data.minecraftProfile.validity = Validity::Certain;
return account;
}
@@ -120,11 +119,11 @@ QPixmap MinecraftAccount::getFace() const
return skin.scaled(64, 64, Qt::KeepAspectRatio);
}
-shared_qobject_ptr MinecraftAccount::loginMSA()
+shared_qobject_ptr MinecraftAccount::login(bool useDeviceCode)
{
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::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); });
@@ -132,29 +131,13 @@ shared_qobject_ptr MinecraftAccount::loginMSA()
return m_currentTask;
}
-shared_qobject_ptr MinecraftAccount::loginOffline()
-{
- 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 MinecraftAccount::refresh()
+shared_qobject_ptr MinecraftAccount::refresh()
{
if (m_currentTask) {
return m_currentTask;
}
- if (data.type == AccountType::MSA) {
- m_currentTask.reset(new MSASilent(&data));
- } else {
- m_currentTask.reset(new OfflineRefresh(&data));
- }
+ m_currentTask.reset(new AuthFlow(&data, AuthFlow::Action::Refresh, this));
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
@@ -163,7 +146,7 @@ shared_qobject_ptr MinecraftAccount::refresh()
return m_currentTask;
}
-shared_qobject_ptr MinecraftAccount::currentTask()
+shared_qobject_ptr MinecraftAccount::currentTask()
{
return m_currentTask;
}
@@ -189,17 +172,17 @@ void MinecraftAccount::authFailed(QString reason)
if (accountType() == AccountType::MSA) {
data.msaToken.token = QString();
data.msaToken.refresh_token = QString();
- data.msaToken.validity = Katabasis::Validity::None;
- data.validity_ = Katabasis::Validity::None;
+ data.msaToken.validity = Validity::None;
+ data.validity_ = Validity::None;
} else {
data.yggdrasilToken.token = QString();
- data.yggdrasilToken.validity = Katabasis::Validity::None;
- data.validity_ = Katabasis::Validity::None;
+ data.yggdrasilToken.validity = Validity::None;
+ data.validity_ = Validity::None;
}
emit changed();
} break;
case AccountTaskState::STATE_FAILED_GONE: {
- data.validity_ = Katabasis::Validity::None;
+ data.validity_ = Validity::None;
emit changed();
} break;
case AccountTaskState::STATE_CREATED:
@@ -229,13 +212,13 @@ bool MinecraftAccount::shouldRefresh() const
return false;
}
switch (data.validity_) {
- case Katabasis::Validity::Certain: {
+ case Validity::Certain: {
break;
}
- case Katabasis::Validity::None: {
+ case Validity::None: {
return false;
}
- case Katabasis::Validity::Assumed: {
+ case Validity::Assumed: {
return true;
}
}
diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h
index f773b3bc9..b5c623a26 100644
--- a/launcher/minecraft/auth/MinecraftAccount.h
+++ b/launcher/minecraft/auth/MinecraftAccount.h
@@ -43,15 +43,13 @@
#include
#include
-#include
-
#include "AccountData.h"
#include "AuthSession.h"
#include "QObjectPtr.h"
#include "Usable.h"
+#include "minecraft/auth/AuthFlow.h"
class Task;
-class AccountTask;
class MinecraftAccount;
using MinecraftAccountPtr = shared_qobject_ptr;
@@ -97,13 +95,11 @@ class MinecraftAccount : public QObject, public Usable {
QJsonObject saveToJson() const;
public: /* manipulation */
- shared_qobject_ptr loginMSA();
+ shared_qobject_ptr login(bool useDeviceCode = false);
- shared_qobject_ptr loginOffline();
+ shared_qobject_ptr refresh();
- shared_qobject_ptr refresh();
-
- shared_qobject_ptr currentTask();
+ shared_qobject_ptr currentTask();
public: /* queries */
QString internalId() const { return data.internalId; }
@@ -166,7 +162,7 @@ class MinecraftAccount : public QObject, public Usable {
AccountData data;
// current task we are executing here
- shared_qobject_ptr m_currentTask;
+ shared_qobject_ptr m_currentTask;
protected: /* methods */
void incrementUses() override;
diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp
index f6179a93e..a2b97e278 100644
--- a/launcher/minecraft/auth/Parsers.cpp
+++ b/launcher/minecraft/auth/Parsers.cpp
@@ -79,7 +79,7 @@ bool getBool(QJsonValue value, bool& out)
// 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 << ":";
qCDebug(authCredentials()) << data;
@@ -135,7 +135,7 @@ bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString nam
qWarning() << "Missing uhs";
return false;
}
- output.validity = Katabasis::Validity::Certain;
+ output.validity = Validity::Certain;
qDebug() << name << "is valid.";
return true;
}
@@ -213,7 +213,7 @@ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output)
output.capes[capeOut.id] = capeOut;
}
output.currentCape = currentCape;
- output.validity = Katabasis::Validity::Certain;
+ output.validity = Validity::Certain;
return true;
}
@@ -388,7 +388,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output)
output.currentCape = capeOut.alias;
}
- output.validity = Katabasis::Validity::Certain;
+ output.validity = Validity::Certain;
return true;
}
@@ -422,7 +422,7 @@ bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output)
output.ownsMinecraft = true;
}
}
- output.validity = Katabasis::Validity::Certain;
+ output.validity = Validity::Certain;
return true;
}
@@ -456,7 +456,7 @@ bool parseRolloutResponse(QByteArray& data, bool& result)
return true;
}
-bool parseMojangResponse(QByteArray& data, Katabasis::Token& output)
+bool parseMojangResponse(QByteArray& data, Token& output)
{
QJsonParseError jsonError;
qDebug() << "Parsing Mojang response...";
@@ -488,7 +488,7 @@ bool parseMojangResponse(QByteArray& data, Katabasis::Token& output)
qWarning() << "access_token is not valid";
return false;
}
- output.validity = Katabasis::Validity::Certain;
+ output.validity = Validity::Certain;
qDebug() << "Mojang response is valid.";
return true;
}
diff --git a/launcher/minecraft/auth/Parsers.h b/launcher/minecraft/auth/Parsers.h
index d073f9994..4a235e4c2 100644
--- a/launcher/minecraft/auth/Parsers.h
+++ b/launcher/minecraft/auth/Parsers.h
@@ -9,8 +9,8 @@ bool getNumber(QJsonValue value, double& out);
bool getNumber(QJsonValue value, int64_t& out);
bool getBool(QJsonValue value, bool& out);
-bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString name);
-bool parseMojangResponse(QByteArray& data, Katabasis::Token& output);
+bool parseXTokenResponse(QByteArray& data, Token& output, QString name);
+bool parseMojangResponse(QByteArray& data, Token& output);
bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output);
bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output);
diff --git a/launcher/minecraft/auth/flows/AuthFlow.cpp b/launcher/minecraft/auth/flows/AuthFlow.cpp
deleted file mode 100644
index c51839a8c..000000000
--- a/launcher/minecraft/auth/flows/AuthFlow.cpp
+++ /dev/null
@@ -1,67 +0,0 @@
-#include
-#include
-#include
-#include
-
-#include "AuthFlow.h"
-#include "katabasis/Globals.h"
-
-#include
-
-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();
- }
-}
diff --git a/launcher/minecraft/auth/flows/AuthFlow.h b/launcher/minecraft/auth/flows/AuthFlow.h
deleted file mode 100644
index e39e926dd..000000000
--- a/launcher/minecraft/auth/flows/AuthFlow.h
+++ /dev/null
@@ -1,41 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include
-
-#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 m_steps;
- AuthStep::Ptr m_currentStep;
-};
diff --git a/launcher/minecraft/auth/flows/MSA.cpp b/launcher/minecraft/auth/flows/MSA.cpp
deleted file mode 100644
index f0399342e..000000000
--- a/launcher/minecraft/auth/flows/MSA.cpp
+++ /dev/null
@@ -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(m_data, MSAStep::Action::Refresh));
- m_steps.append(makeShared(m_data));
- m_steps.append(makeShared(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
- m_steps.append(makeShared(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
- m_steps.append(makeShared(m_data));
- m_steps.append(makeShared(m_data));
- m_steps.append(makeShared(m_data));
- m_steps.append(makeShared(m_data));
- m_steps.append(makeShared(m_data));
-}
-
-MSAInteractive::MSAInteractive(AccountData* data, QObject* parent) : AuthFlow(data, parent)
-{
- m_steps.append(makeShared(m_data, MSAStep::Action::Login));
- m_steps.append(makeShared(m_data));
- m_steps.append(makeShared(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
- m_steps.append(makeShared(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
- m_steps.append(makeShared(m_data));
- m_steps.append(makeShared(m_data));
- m_steps.append(makeShared(m_data));
- m_steps.append(makeShared(m_data));
- m_steps.append(makeShared(m_data));
-}
diff --git a/launcher/minecraft/auth/flows/MSA.h b/launcher/minecraft/auth/flows/MSA.h
deleted file mode 100644
index e403d530f..000000000
--- a/launcher/minecraft/auth/flows/MSA.h
+++ /dev/null
@@ -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);
-};
diff --git a/launcher/minecraft/auth/flows/Offline.cpp b/launcher/minecraft/auth/flows/Offline.cpp
deleted file mode 100644
index 3770b869a..000000000
--- a/launcher/minecraft/auth/flows/Offline.cpp
+++ /dev/null
@@ -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(m_data));
-}
-
-OfflineLogin::OfflineLogin(AccountData* data, QObject* parent) : AuthFlow(data, parent)
-{
- m_steps.append(makeShared(m_data));
-}
diff --git a/launcher/minecraft/auth/flows/Offline.h b/launcher/minecraft/auth/flows/Offline.h
deleted file mode 100644
index 2bc9c7612..000000000
--- a/launcher/minecraft/auth/flows/Offline.h
+++ /dev/null
@@ -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);
-};
diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/launcher/minecraft/auth/steps/EntitlementsStep.cpp
index 0573dcb6e..19cbe6898 100644
--- a/launcher/minecraft/auth/steps/EntitlementsStep.cpp
+++ b/launcher/minecraft/auth/steps/EntitlementsStep.cpp
@@ -1,16 +1,20 @@
#include "EntitlementsStep.h"
+#include
#include
+#include
#include
+#include
+#include "Application.h"
#include "Logging.h"
-#include "minecraft/auth/AuthRequest.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() noexcept = default;
-
QString EntitlementsStep::describe()
{
return tr("Determining game ownership.");
@@ -19,35 +23,31 @@ QString EntitlementsStep::describe()
void EntitlementsStep::perform()
{
auto uuid = QUuid::createUuid();
- m_entitlementsRequestId = uuid.toString().remove('{').remove('}');
- auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlementsRequestId;
- QNetworkRequest request = QNetworkRequest(url);
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- request.setRawHeader("Accept", "application/json");
- request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
- AuthRequest* requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &EntitlementsStep::onRequestDone);
- requestor->get(request);
+ m_entitlements_request_id = uuid.toString().remove('{').remove('}');
+
+ QUrl url("https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlements_request_id);
+ auto headers = QList{ { "Content-Type", "application/json" },
+ { "Accept", "application/json" },
+ { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } };
+
+ m_response.reset(new QByteArray());
+ 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...";
}
-void EntitlementsStep::rehydrate()
+void EntitlementsStep::onRequestDone()
{
- // NOOP, for now. We only save bools and there's nothing to check.
-}
-
-void EntitlementsStep::onRequestDone([[maybe_unused]] QNetworkReply::NetworkError error,
- QByteArray data,
- [[maybe_unused]] QList headers)
-{
- auto requestor = qobject_cast(QObject::sender());
- requestor->deleteLater();
-
- qCDebug(authCredentials()) << data;
+ qCDebug(authCredentials()) << *m_response;
// TODO: check presence of same entitlementsRequestId?
// TODO: validate JWTs?
- Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement);
+ Parsers::parseMinecraftEntitlements(*m_response, m_data->minecraftEntitlement);
emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements"));
}
diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.h b/launcher/minecraft/auth/steps/EntitlementsStep.h
index be16bda13..dd8ec7aaa 100644
--- a/launcher/minecraft/auth/steps/EntitlementsStep.h
+++ b/launcher/minecraft/auth/steps/EntitlementsStep.h
@@ -1,24 +1,26 @@
#pragma once
#include
+#include
-#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
+#include "net/Download.h"
class EntitlementsStep : public AuthStep {
Q_OBJECT
public:
explicit EntitlementsStep(AccountData* data);
- virtual ~EntitlementsStep() noexcept;
+ virtual ~EntitlementsStep() noexcept = default;
void perform() override;
- void rehydrate() override;
QString describe() override;
private slots:
- void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList);
+ void onRequestDone();
private:
- QString m_entitlementsRequestId;
+ QString m_entitlements_request_id;
+ std::shared_ptr m_response;
+ Net::Download::Ptr m_task;
};
diff --git a/launcher/minecraft/auth/steps/GetSkinStep.cpp b/launcher/minecraft/auth/steps/GetSkinStep.cpp
index 520877020..d9785b16a 100644
--- a/launcher/minecraft/auth/steps/GetSkinStep.cpp
+++ b/launcher/minecraft/auth/steps/GetSkinStep.cpp
@@ -3,13 +3,10 @@
#include
-#include "minecraft/auth/AuthRequest.h"
-#include "minecraft/auth/Parsers.h"
+#include "Application.h"
GetSkinStep::GetSkinStep(AccountData* data) : AuthStep(data) {}
-GetSkinStep::~GetSkinStep() noexcept = default;
-
QString GetSkinStep::describe()
{
return tr("Getting skin.");
@@ -17,25 +14,20 @@ QString GetSkinStep::describe()
void GetSkinStep::perform()
{
- auto url = QUrl(m_data->minecraftProfile.skin.url);
- QNetworkRequest request = QNetworkRequest(url);
- AuthRequest* requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &GetSkinStep::onRequestDone);
- requestor->get(request);
+ QUrl url(m_data->minecraftProfile.skin.url);
+
+ m_response.reset(new QByteArray());
+ m_task = Net::Download::makeByteArray(url, m_response);
+
+ 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.
-}
-
-void GetSkinStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList headers)
-{
- auto requestor = qobject_cast(QObject::sender());
- requestor->deleteLater();
-
- if (error == QNetworkReply::NoError) {
- m_data->minecraftProfile.skin.data = data;
- }
+ if (m_task->error() == QNetworkReply::NoError)
+ m_data->minecraftProfile.skin.data = *m_response;
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin"));
}
diff --git a/launcher/minecraft/auth/steps/GetSkinStep.h b/launcher/minecraft/auth/steps/GetSkinStep.h
index 105e497d1..fffd8be03 100644
--- a/launcher/minecraft/auth/steps/GetSkinStep.h
+++ b/launcher/minecraft/auth/steps/GetSkinStep.h
@@ -1,21 +1,25 @@
#pragma once
#include
+#include
-#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
+#include "net/Download.h"
class GetSkinStep : public AuthStep {
Q_OBJECT
public:
explicit GetSkinStep(AccountData* data);
- virtual ~GetSkinStep() noexcept;
+ virtual ~GetSkinStep() noexcept = default;
void perform() override;
- void rehydrate() override;
QString describe() override;
private slots:
- void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList);
+ void onRequestDone();
+
+ private:
+ std::shared_ptr m_response;
+ Net::Download::Ptr m_task;
};
diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp
index c57f51113..d72346c74 100644
--- a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp
+++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp
@@ -1,17 +1,17 @@
#include "LauncherLoginStep.h"
#include
+#include
+#include "Application.h"
#include "Logging.h"
-#include "minecraft/auth/AccountTask.h"
-#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
+#include "net/StaticHeaderProxy.h"
+#include "net/Upload.h"
LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {}
-LauncherLoginStep::~LauncherLoginStep() noexcept = default;
-
QString LauncherLoginStep::describe()
{
return tr("Accessing Mojang services.");
@@ -19,7 +19,7 @@ QString LauncherLoginStep::describe()
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 xToken = m_data->mojangservicesToken.token;
@@ -31,40 +31,37 @@ void LauncherLoginStep::perform()
)XXX";
auto requestBody = mc_auth_template.arg(uhs, xToken);
- QNetworkRequest request = QNetworkRequest(QUrl(requestURL));
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- request.setRawHeader("Accept", "application/json");
- AuthRequest* requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &LauncherLoginStep::onRequestDone);
- requestor->post(request, requestBody.toUtf8());
+ auto headers = QList{
+ { "Content-Type", "application/json" },
+ { "Accept", "application/json" },
+ };
+
+ 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...";
}
-void LauncherLoginStep::rehydrate()
+void LauncherLoginStep::onRequestDone()
{
- // TODO: check the token validity
-}
-
-void LauncherLoginStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList headers)
-{
- auto requestor = qobject_cast(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_));
+ qCDebug(authCredentials()) << *m_response;
+ if (m_task->error() != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << m_task->error();
+ if (Net::isApplicationError(m_task->error())) {
+ emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get Minecraft access token: %1").arg(m_task->errorString()));
} 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;
}
- if (!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) {
+ if (!Parsers::parseMojangResponse(*m_response, m_data->yggdrasilToken)) {
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."));
return;
}
diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.h b/launcher/minecraft/auth/steps/LauncherLoginStep.h
index 30c18e675..21a2a4920 100644
--- a/launcher/minecraft/auth/steps/LauncherLoginStep.h
+++ b/launcher/minecraft/auth/steps/LauncherLoginStep.h
@@ -1,21 +1,25 @@
#pragma once
#include
+#include
-#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
+#include "net/Upload.h"
class LauncherLoginStep : public AuthStep {
Q_OBJECT
public:
explicit LauncherLoginStep(AccountData* data);
- virtual ~LauncherLoginStep() noexcept;
+ virtual ~LauncherLoginStep() noexcept = default;
void perform() override;
- void rehydrate() override;
QString describe() override;
private slots:
- void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList);
+ void onRequestDone();
+
+ private:
+ std::shared_ptr m_response;
+ Net::Upload::Ptr m_task;
};
diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp
new file mode 100644
index 000000000..22f5d4069
--- /dev/null
+++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp
@@ -0,0 +1,270 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2024 Trial97
+ *
+ * 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 .
+ *
+ * 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
+#include
+
+#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{
+ { "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{
+ { "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"));
+}
\ No newline at end of file
diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h
new file mode 100644
index 000000000..e53eebc62
--- /dev/null
+++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2024 Trial97
+ *
+ * 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 .
+ *
+ * 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
+#include
+
+#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 m_response;
+ Net::Upload::Ptr m_task;
+};
diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp
index 1aa22765d..3f31cdc16 100644
--- a/launcher/minecraft/auth/steps/MSAStep.cpp
+++ b/launcher/minecraft/auth/steps/MSAStep.cpp
@@ -35,123 +35,74 @@
#include "MSAStep.h"
+#include
+#include
#include
-#include "BuildConfig.h"
-#include "minecraft/auth/AuthRequest.h"
-#include "minecraft/auth/Parsers.h"
-
#include "Application.h"
-#include "Logging.h"
-using OAuth2 = Katabasis::DeviceFlow;
-using Activity = Katabasis::Activity;
-
-MSAStep::MSAStep(AccountData* data, Action action) : AuthStep(data), m_action(action)
+MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(silent)
{
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
- m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get());
+ auto replyHandler = new QOAuthHttpServerReplyHandler(1337, this);
+ replyHandler->setCallbackText(
+ " ");
+ 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(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &MSAStep::showVerificationUriAndCode);
+ connect(&oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this] {
+ 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()
{
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()
{
- switch (m_action) {
- case Refresh: {
- if (m_data->msaClientID != m_clientId) {
- emit hideVerificationUriAndCode();
- emit finished(AccountTaskState::STATE_DISABLED,
- tr("Microsoft user authentication failed - client identification has changed."));
- }
- m_oauth2->refresh();
- return;
+ if (m_silent) {
+ if (m_data->msaClientID != m_clientId) {
+ emit finished(AccountTaskState::STATE_DISABLED,
+ tr("Microsoft user authentication failed - client identification has changed."));
}
- case Login: {
- QVariantMap extraOpts;
- extraOpts["prompt"] = "select_account";
- m_oauth2->setExtraRequestParams(extraOpts);
+ oauth2.setRefreshToken(m_data->msaToken.refresh_token);
+ oauth2.refreshAccessToken();
+ } else {
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) // QMultiMap param changed in 6.0
+ oauth2.setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMultiMap* map) {
+#else
+ oauth2.setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMap* map) {
+#endif
+ map->insert("prompt", "select_account");
+ });
- *m_data = AccountData();
- m_data->msaClientID = m_clientId;
- m_oauth2->login();
- 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;
- }
+ *m_data = AccountData();
+ m_data->msaClientID = m_clientId;
+ oauth2.grant();
}
}
diff --git a/launcher/minecraft/auth/steps/MSAStep.h b/launcher/minecraft/auth/steps/MSAStep.h
index b6635d4a5..675cfb2ca 100644
--- a/launcher/minecraft/auth/steps/MSAStep.h
+++ b/launcher/minecraft/auth/steps/MSAStep.h
@@ -36,30 +36,24 @@
#pragma once
#include
-#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
-#include
-
+#include
class MSAStep : public AuthStep {
Q_OBJECT
public:
- enum Action { Refresh, Login };
-
- public:
- explicit MSAStep(AccountData* data, Action action);
- virtual ~MSAStep() noexcept;
+ explicit MSAStep(AccountData* data, bool silent = false);
+ virtual ~MSAStep() noexcept = default;
void perform() override;
- void rehydrate() override;
QString describe() override;
- private slots:
- void onOAuthActivityChanged(Katabasis::Activity activity);
+ signals:
+ void authorizeWithBrowser(const QUrl& url);
private:
- Katabasis::DeviceFlow* m_oauth2 = nullptr;
- Action m_action;
+ bool m_silent;
QString m_clientId;
+ QOAuth2AuthorizationCodeFlow oauth2;
};
diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
index a854342bc..305f44320 100644
--- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
+++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
@@ -2,15 +2,13 @@
#include
-#include "Logging.h"
-#include "minecraft/auth/AuthRequest.h"
+#include "Application.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
+#include "net/StaticHeaderProxy.h"
MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) {}
-MinecraftProfileStep::~MinecraftProfileStep() noexcept = default;
-
QString MinecraftProfileStep::describe()
{
return tr("Fetching the Minecraft profile.");
@@ -18,52 +16,47 @@ QString MinecraftProfileStep::describe()
void MinecraftProfileStep::perform()
{
- auto url = QUrl("https://api.minecraftservices.com/minecraft/profile");
- QNetworkRequest request = QNetworkRequest(url);
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
+ QUrl url("https://api.minecraftservices.com/minecraft/profile");
+ auto headers = QList{ { "Content-Type", "application/json" },
+ { "Accept", "application/json" },
+ { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } };
- AuthRequest* requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &MinecraftProfileStep::onRequestDone);
- requestor->get(request);
+ m_response.reset(new QByteArray());
+ m_task = Net::Download::makeByteArray(url, m_response);
+ 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.
-}
-
-void MinecraftProfileStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList headers)
-{
- auto requestor = qobject_cast(QObject::sender());
- requestor->deleteLater();
-
- qCDebug(authCredentials()) << data;
- if (error == QNetworkReply::ContentNotFoundError) {
+ if (m_task->error() == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state.
m_data->minecraftProfile = MinecraftProfile();
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Account has no Minecraft profile."));
return;
}
- if (error != QNetworkReply::NoError) {
+ if (m_task->error() != QNetworkReply::NoError) {
qWarning() << "Error getting profile:";
- qWarning() << " HTTP Status: " << requestor->httpStatus_;
- qWarning() << " Internal error no.: " << error;
- qWarning() << " Error string: " << requestor->errorString_;
+ qWarning() << " HTTP Status: " << m_task->replyStatusCode();
+ qWarning() << " Internal error no.: " << m_task->error();
+ qWarning() << " Error string: " << m_task->errorString();
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,
- tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_));
+ tr("Minecraft Java profile acquisition failed: %1").arg(m_task->errorString()));
} else {
- emit finished(AccountTaskState::STATE_OFFLINE,
- tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_));
+ emit finished(AccountTaskState::STATE_OFFLINE, tr("Minecraft Java profile acquisition failed: %1").arg(m_task->errorString()));
}
return;
}
- if (!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) {
+ if (!Parsers::parseMinecraftProfile(*m_response, m_data->minecraftProfile)) {
m_data->minecraftProfile = MinecraftProfile();
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile response could not be parsed"));
return;
diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.h b/launcher/minecraft/auth/steps/MinecraftProfileStep.h
index cb30dab21..831cd52f7 100644
--- a/launcher/minecraft/auth/steps/MinecraftProfileStep.h
+++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.h
@@ -1,21 +1,25 @@
#pragma once
#include
+#include
-#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
+#include "net/Download.h"
class MinecraftProfileStep : public AuthStep {
Q_OBJECT
public:
explicit MinecraftProfileStep(AccountData* data);
- virtual ~MinecraftProfileStep() noexcept;
+ virtual ~MinecraftProfileStep() noexcept = default;
void perform() override;
- void rehydrate() override;
QString describe() override;
private slots:
- void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList);
+ void onRequestDone();
+
+ private:
+ std::shared_ptr m_response;
+ Net::Download::Ptr m_task;
};
diff --git a/launcher/minecraft/auth/steps/OfflineStep.cpp b/launcher/minecraft/auth/steps/OfflineStep.cpp
deleted file mode 100644
index bf111abe8..000000000
--- a/launcher/minecraft/auth/steps/OfflineStep.cpp
+++ /dev/null
@@ -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."));
-}
diff --git a/launcher/minecraft/auth/steps/OfflineStep.h b/launcher/minecraft/auth/steps/OfflineStep.h
deleted file mode 100644
index 3bf123d6a..000000000
--- a/launcher/minecraft/auth/steps/OfflineStep.h
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma once
-#include
-
-#include "QObjectPtr.h"
-#include "minecraft/auth/AuthStep.h"
-
-#include
-
-class OfflineStep : public AuthStep {
- Q_OBJECT
- public:
- explicit OfflineStep(AccountData* data);
- virtual ~OfflineStep() noexcept;
-
- void perform() override;
- void rehydrate() override;
-
- QString describe() override;
-};
diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
index c33d7e629..f07220986 100644
--- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
+++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
@@ -4,27 +4,22 @@
#include
#include
+#include "Application.h"
#include "Logging.h"
-#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.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)
{}
-XboxAuthorizationStep::~XboxAuthorizationStep() noexcept = default;
-
QString XboxAuthorizationStep::describe()
{
return tr("Getting authorization to access %1 services.").arg(m_authorizationKind);
}
-void XboxAuthorizationStep::rehydrate()
-{
- // FIXME: check if the tokens are good?
-}
-
void XboxAuthorizationStep::perform()
{
QString xbox_auth_template = R"XXX(
@@ -41,40 +36,44 @@ void XboxAuthorizationStep::perform()
)XXX";
auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token, m_relyingParty);
// http://xboxlive.com
- QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- request.setRawHeader("Accept", "application/json");
- AuthRequest* requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &XboxAuthorizationStep::onRequestDone);
- requestor->post(request, xbox_auth_data.toUtf8());
+ QUrl url("https://xsts.auth.xboxlive.com/xsts/authorize");
+ auto headers = QList{
+ { "Content-Type", "application/json" },
+ { "Accept", "application/json" },
+ };
+ 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;
}
-void XboxAuthorizationStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList headers)
+void XboxAuthorizationStep::onRequestDone()
{
- auto requestor = qobject_cast(QObject::sender());
- requestor->deleteLater();
-
- qCDebug(authCredentials()) << data;
- if (error != QNetworkReply::NoError) {
- qWarning() << "Reply error:" << error;
- if (Net::isApplicationError(error)) {
- if (!processSTSError(error, data, headers)) {
+ qCDebug(authCredentials()) << *m_response;
+ if (m_task->error() != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << m_task->error();
+ if (Net::isApplicationError(m_task->error())) {
+ if (!processSTSError()) {
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 {
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 {
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;
}
- Katabasis::Token temp;
- if (!Parsers::parseXTokenResponse(data, temp, m_authorizationKind)) {
+ Token temp;
+ if (!Parsers::parseXTokenResponse(*m_response, temp, m_authorizationKind)) {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Could not parse authorization response for access to %1 services.").arg(m_authorizationKind));
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));
}
-bool XboxAuthorizationStep::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList headers)
+bool XboxAuthorizationStep::processSTSError()
{
- if (error == QNetworkReply::AuthenticationRequiredError) {
+ if (m_task->error() == QNetworkReply::AuthenticationRequiredError) {
QJsonParseError jsonError;
- QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ QJsonDocument doc = QJsonDocument::fromJson(*m_response, &jsonError);
if (jsonError.error) {
qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString();
emit finished(AccountTaskState::STATE_FAILED_SOFT,
@@ -126,7 +125,35 @@ bool XboxAuthorizationStep::processSTSError(QNetworkReply::NetworkError error, Q
emit finished(
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.")
- .arg("help.minecraft.net"));
+ .arg("help.minecraft.net"));
+ 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("login.live.com"));
+ 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("account.microsoft.com"));
+ 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;
}
default: {
diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.h b/launcher/minecraft/auth/steps/XboxAuthorizationStep.h
index dee24c954..f6329b7f0 100644
--- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.h
+++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.h
@@ -1,29 +1,32 @@
#pragma once
#include
+#include
-#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
+#include "net/Upload.h"
class XboxAuthorizationStep : public AuthStep {
Q_OBJECT
public:
- explicit XboxAuthorizationStep(AccountData* data, Katabasis::Token* token, QString relyingParty, QString authorizationKind);
- virtual ~XboxAuthorizationStep() noexcept;
+ explicit XboxAuthorizationStep(AccountData* data, Token* token, QString relyingParty, QString authorizationKind);
+ virtual ~XboxAuthorizationStep() noexcept = default;
void perform() override;
- void rehydrate() override;
QString describe() override;
private:
- bool processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList headers);
+ bool processSTSError();
private slots:
- void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList);
+ void onRequestDone();
private:
- Katabasis::Token* m_token;
+ Token* m_token;
QString m_relyingParty;
QString m_authorizationKind;
+
+ std::shared_ptr m_response;
+ Net::Upload::Ptr m_task;
};
diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/launcher/minecraft/auth/steps/XboxProfileStep.cpp
index fd2b32cce..440a4657c 100644
--- a/launcher/minecraft/auth/steps/XboxProfileStep.cpp
+++ b/launcher/minecraft/auth/steps/XboxProfileStep.cpp
@@ -3,28 +3,21 @@
#include
#include
+#include "Application.h"
#include "Logging.h"
-#include "minecraft/auth/AuthRequest.h"
-#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
+#include "net/StaticHeaderProxy.h"
XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) {}
-XboxProfileStep::~XboxProfileStep() noexcept = default;
-
QString XboxProfileStep::describe()
{
return tr("Fetching Xbox profile.");
}
-void XboxProfileStep::rehydrate()
-{
- // NOOP, for now. We only save bools and there's nothing to check.
-}
-
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;
q.addQueryItem("settings",
"GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
@@ -33,36 +26,38 @@ void XboxProfileStep::perform()
"PreferredColor,Location,Bio,Watermarks,"
"RealName,RealNameOverride,IsQuarantined");
url.setQuery(q);
+ auto headers = QList