diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 528b128b1..c7d36db27 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -5,3 +5,9 @@ bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9
# (nix) alejandra -> nixfmt
4c81d8c53d09196426568c4a31a4e752ed05397a
+
+# reformat codebase
+1d468ac35ad88d8c77cc83f25e3704d9bd7df01b
+
+# format a part of codebase
+5c8481a118c8fefbfe901001d7828eaf6866eac4
diff --git a/.github/actions/package/linux/action.yml b/.github/actions/package/linux/action.yml
new file mode 100644
index 000000000..b71e62592
--- /dev/null
+++ b/.github/actions/package/linux/action.yml
@@ -0,0 +1,124 @@
+name: Package for Linux
+description: Create Linux packages for Prism Launcher
+
+inputs:
+ version:
+ description: Launcher version
+ required: true
+ build-type:
+ description: Type for the build
+ required: true
+ default: Debug
+ artifact-name:
+ description: Name of the uploaded artifact
+ required: true
+ default: Linux
+ cmake-preset:
+ description: Base CMake preset previously used for the build
+ required: true
+ default: linux
+ qt-version:
+ description: Version of Qt to use
+ required: true
+ gpg-private-key:
+ description: Private key for AppImage signing
+ required: false
+ gpg-private-key-id:
+ description: ID for the gpg-private-key, to select the signing key
+ required: false
+
+runs:
+ using: composite
+
+ steps:
+ - name: Package AppImage
+ shell: bash
+ env:
+ VERSION: ${{ inputs.version }}
+ BUILD_DIR: build
+ INSTALL_APPIMAGE_DIR: install-appdir
+
+ GPG_PRIVATE_KEY: ${{ inputs.gpg-private-key }}
+ run: |
+ cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr
+
+ mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml
+ export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated
+
+ export OUTPUT="PrismLauncher-Linux-x86_64.AppImage"
+
+ chmod +x linuxdeploy-*.AppImage
+
+ mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib
+ mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
+
+ cp -r ${{ runner.workspace }}/Qt/${{ inputs.qt-version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
+
+ cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
+ cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
+ cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
+
+ LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
+ export LD_LIBRARY_PATH
+
+ chmod +x AppImageUpdate-x86_64.AppImage
+ cp AppImageUpdate-x86_64.AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin
+
+ export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-x86_64.AppImage.zsync"
+
+ if [ '${{ inputs.gpg-private-key-id }}' != '' ]; then
+ export SIGN=1
+ export SIGN_KEY=${{ inputs.gpg-private-key-id }}
+ mkdir -p ~/.gnupg/
+ echo "$GPG_PRIVATE_KEY" > ~/.gnupg/private.key
+ gpg --import ~/.gnupg/private.key
+ else
+ echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY
+ fi
+
+ ./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.prismlauncher.PrismLauncher.svg
+
+ mv "PrismLauncher-Linux-x86_64.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build-type }}-x86_64.AppImage"
+
+ - name: Package portable tarball
+ shell: bash
+ env:
+ BUILD_DIR: build
+
+ CMAKE_PRESET: ${{ inputs.cmake-preset }}
+
+ INSTALL_PORTABLE_DIR: install-portable
+ run: |
+ cmake --preset "$CMAKE_PRESET" -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_PORTABLE_DIR }} -DINSTALL_BUNDLE=full
+ 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.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
+ cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
+ cp /usr/lib/x86_64-linux-gnu/libffi.so.*.* ${{ 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 *
+
+ - name: Upload binary tarball
+ uses: actions/upload-artifact@v4
+ with:
+ name: PrismLauncher-${{ inputs.artifact-name }}-Qt6-Portable-${{ inputs.version }}-${{ inputs.build-type }}
+ path: PrismLauncher-portable.tar.gz
+
+ - name: Upload AppImage
+ uses: actions/upload-artifact@v4
+ with:
+ name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage
+ path: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage
+
+ - name: Upload AppImage Zsync
+ uses: actions/upload-artifact@v4
+ with:
+ name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage.zsync
+ path: PrismLauncher-Linux-x86_64.AppImage.zsync
diff --git a/.github/actions/package/macos/action.yml b/.github/actions/package/macos/action.yml
new file mode 100644
index 000000000..42181953c
--- /dev/null
+++ b/.github/actions/package/macos/action.yml
@@ -0,0 +1,121 @@
+name: Package for macOS
+description: Create a macOS package for Prism Launcher
+
+inputs:
+ version:
+ description: Launcher version
+ required: true
+ build-type:
+ description: Type for the build
+ required: true
+ default: Debug
+ artifact-name:
+ description: Name of the uploaded artifact
+ required: true
+ default: macOS
+ 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
+ sparkle-ed25519-key:
+ description: Private key for signing Sparkle updates
+ required: false
+
+runs:
+ using: composite
+
+ steps:
+ - name: Fetch codesign certificate
+ shell: bash
+ run: |
+ echo '${{ inputs.apple-codesign-cert }}' | base64 --decode > codesign.p12
+ if [ -n '${{ inputs.apple-codesign-id }}' ]; then
+ security create-keychain -p '${{ inputs.apple-codesign-password }}' build.keychain
+ security default-keychain -s build.keychain
+ security unlock-keychain -p '${{ inputs.apple-codesign-password }}' build.keychain
+ security import codesign.p12 -k build.keychain -P '${{ inputs.apple-codesign-password }}' -T /usr/bin/codesign
+ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k '${{ inputs.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
+ shell: bash
+ env:
+ BUILD_DIR: build
+ INSTALL_DIR: install
+ run: |
+ cmake --install ${{ env.BUILD_DIR }}
+
+ cd ${{ env.INSTALL_DIR }}
+ chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher"
+
+ if [ -n '${{ inputs.apple-codesign-id }}' ]; then
+ APPLE_CODESIGN_ID='${{ inputs.apple-codesign-id }}'
+ ENTITLEMENTS_FILE='../program_info/App.entitlements'
+ else
+ APPLE_CODESIGN_ID='-'
+ ENTITLEMENTS_FILE='../program_info/AdhocSignedApp.entitlements'
+ fi
+
+ sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "$ENTITLEMENTS_FILE" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
+ mv "PrismLauncher.app" "Prism Launcher.app"
+
+ - name: Notarize
+ shell: bash
+ env:
+ INSTALL_DIR: install
+ run: |
+ cd ${{ env.INSTALL_DIR }}
+
+ if [ -n '${{ inputs.apple-notarize-password }}' ]; then
+ ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip
+ xcrun notarytool submit ../PrismLauncher.zip \
+ --wait --progress \
+ --apple-id '${{ inputs.apple-notarize-apple-id }}' \
+ --team-id '${{ inputs.apple-notarize-team-id }}' \
+ --password '${{ inputs.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
+ shell: bash
+ run: |
+ if [ '${{ inputs.sparkle-ed25519-key }}' != '' ]; then
+ echo '${{ inputs.sparkle-ed25519-key }}' > ed25519-priv.pem
+ signature=$(/opt/homebrew/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:
+ - :memo: Sparkle Signature (ed25519): \`$signature\`
+ EOF
+ else
+ cat >> $GITHUB_STEP_SUMMARY << EOF
+ ### Artifact Information :information_source:
+ - :warning: Sparkle Signature (ed25519): No private key available (likely a pull request or fork)
+ EOF
+ fi
+
+ - name: Upload binary tarball
+ uses: actions/upload-artifact@v4
+ with:
+ name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}
+ path: PrismLauncher.zip
diff --git a/.github/actions/package/windows/action.yml b/.github/actions/package/windows/action.yml
new file mode 100644
index 000000000..60b2c75d1
--- /dev/null
+++ b/.github/actions/package/windows/action.yml
@@ -0,0 +1,143 @@
+name: Package for Windows
+description: Create a Windows package for Prism Launcher
+
+inputs:
+ version:
+ description: Launcher version
+ required: true
+ build-type:
+ description: Type for the build
+ required: true
+ default: Debug
+ artifact-name:
+ description: Name of the uploaded artifact
+ required: true
+ msystem:
+ description: MSYS2 subsystem to use
+ required: true
+ default: false
+ windows-codesign-cert:
+ description: Certificate for signing Windows builds
+ required: false
+ windows-codesign-password:
+ description: Password for signing Windows builds
+ required: false
+
+runs:
+ using: composite
+
+ steps:
+ - name: Package (MinGW)
+ if: ${{ inputs.msystem != '' }}
+ shell: msys2 {0}
+ env:
+ BUILD_DIR: build
+ INSTALL_DIR: install
+ run: |
+ cmake --install ${{ env.BUILD_DIR }}
+ touch ${{ env.INSTALL_DIR }}/manifest.txt
+ for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt
+
+ - name: Package (MSVC)
+ if: ${{ inputs.msystem == '' }}
+ shell: pwsh
+ env:
+ BUILD_DIR: build
+ INSTALL_DIR: install
+ run: |
+ cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }}
+
+ 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
+
+ - name: Fetch codesign certificate
+ shell: bash # yes, we are not using MSYS2 or PowerShell here
+ run: |
+ echo '${{ inputs.windows-codesign-cert }}' | base64 --decode > codesign.pfx
+
+ - name: Sign executable
+ shell: pwsh
+ env:
+ INSTALL_DIR: install
+ run: |
+ if (Get-Content ./codesign.pfx){
+ cd ${{ env.INSTALL_DIR }}
+ # We ship the exact same executable for portable and non-portable editions, so signing just once is fine
+ SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ inputs.windows-codesign-password }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_updater.exe prismlauncher_filelink.exe
+ } else {
+ ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
+ }
+
+ - name: Package (MinGW, portable)
+ if: ${{ inputs.msystem != '' }}
+ shell: msys2 {0}
+ env:
+ BUILD_DIR: build
+ INSTALL_DIR: install
+ INSTALL_PORTABLE_DIR: install-portable
+ run: |
+ cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
+ cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
+ for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
+
+ - name: Package (MSVC, portable)
+ if: ${{ inputs.msystem == '' }}
+ shell: pwsh
+ env:
+ BUILD_DIR: build
+ INSTALL_DIR: install
+ INSTALL_PORTABLE_DIR: install-portable
+ run: |
+ cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
+ cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
+
+ Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
+
+ - name: Package (installer)
+ shell: pwsh
+ env:
+ BUILD_DIR: build
+ INSTALL_DIR: install
+
+ NSCURL_VERSION: "v24.9.26.122"
+ NSCURL_SHA256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
+ run: |
+ New-Item -Name NSISPlugins -ItemType Directory
+ Invoke-Webrequest https://github.com/negrutiu/nsis-nscurl/releases/download/"${{ env.NSCURL_VERSION }}"/NScurl.zip -OutFile NSISPlugins\NScurl.zip
+ $nscurl_hash = Get-FileHash NSISPlugins\NScurl.zip -Algorithm Sha256 | Select-Object -ExpandProperty Hash
+ if ( $nscurl_hash -ne "${{ env.nscurl_sha256 }}") {
+ echo "::error:: NSCurl.zip sha256 mismatch"
+ exit 1
+ }
+ Expand-Archive -Path NSISPlugins\NScurl.zip -DestinationPath NSISPlugins\NScurl
+
+ cd ${{ env.INSTALL_DIR }}
+ makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi"
+
+ - name: Sign installer
+ shell: pwsh
+ run: |
+ if (Get-Content ./codesign.pfx){
+ SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ inputs.windows-codesign-password }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe
+ } else {
+ ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
+ }
+
+ - name: Upload binary zip
+ uses: actions/upload-artifact@v4
+ with:
+ name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}
+ path: install/**
+
+ - name: Upload portable zip
+ uses: actions/upload-artifact@v4
+ with:
+ name: PrismLauncher-${{ inputs.artifact-name }}-Portable-${{ inputs.version }}-${{ inputs.build-type }}
+ path: install-portable/**
+
+ - name: Upload installer
+ uses: actions/upload-artifact@v4
+ with:
+ name: PrismLauncher-${{ inputs.artifact-name }}-Setup-${{ inputs.version }}-${{ inputs.build-type }}
+ path: PrismLauncher-Setup.exe
diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml
new file mode 100644
index 000000000..e97abd1df
--- /dev/null
+++ b/.github/actions/setup-dependencies/action.yml
@@ -0,0 +1,78 @@
+name: Setup Dependencies
+description: Install and setup dependencies for building Prism Launcher
+
+inputs:
+ build-type:
+ description: Type for the build
+ required: true
+ default: Debug
+ msystem:
+ description: MSYS2 subsystem to use
+ required: false
+ vcvars-arch:
+ description: Visual Studio architecture to use
+ required: false
+ qt-architecture:
+ description: Qt architecture
+ required: false
+ qt-version:
+ description: Version of Qt to use
+ required: true
+ default: 6.8.1
+
+outputs:
+ build-type:
+ description: Type of build used
+ value: ${{ inputs.build-type }}
+ qt-version:
+ description: Version of Qt used
+ value: ${{ inputs.qt-version }}
+
+runs:
+ using: composite
+
+ steps:
+ - name: Setup Linux dependencies
+ if: ${{ runner.os == 'Linux' }}
+ uses: ./.github/actions/setup-dependencies/linux
+
+ - name: Setup macOS dependencies
+ if: ${{ runner.os == 'macOS' }}
+ uses: ./.github/actions/setup-dependencies/macos
+
+ - name: Setup Windows dependencies
+ if: ${{ runner.os == 'Windows' }}
+ uses: ./.github/actions/setup-dependencies/windows
+ with:
+ build-type: ${{ inputs.build-type }}
+ msystem: ${{ inputs.msystem }}
+ vcvars-arch: ${{ inputs.vcvars-arch }}
+
+ # TODO(@getchoo): Get this working on MSYS2!
+ - name: Setup ccache
+ if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }}
+ uses: hendrikmuhs/ccache-action@v1.2.18
+ with:
+ variant: ${{ runner.os == 'Windows' && 'sccache' || 'ccache' }}
+ create-symlink: ${{ runner.os != 'Windows' }}
+ key: ${{ runner.os }}-qt${{ inputs.qt_ver }}-${{ inputs.architecture }}
+
+ - name: Use ccache on debug builds
+ if: ${{ inputs.build-type == 'Debug' }}
+ shell: bash
+ env:
+ # Only use sccache on MSVC
+ CCACHE_VARIANT: ${{ (runner.os == 'Windows' && inputs.msystem == '') && 'sccache' || 'ccache' }}
+ run: |
+ echo "CMAKE_C_COMPILER_LAUNCHER=$CCACHE_VARIANT" >> "$GITHUB_ENV"
+ echo "CMAKE_CXX_COMPILER_LAUNCHER=$CCACHE_VARIANT" >> "$GITHUB_ENV"
+
+ - name: Install Qt
+ if: ${{ inputs.msystem == '' }}
+ uses: jurplel/install-qt-action@v4
+ with:
+ aqtversion: "==3.1.*"
+ version: ${{ inputs.qt-version }}
+ arch: ${{ inputs.qt-architecture }}
+ modules: qt5compat qtimageformats qtnetworkauth
+ cache: ${{ inputs.build-type == 'Debug' }}
diff --git a/.github/actions/setup-dependencies/linux/action.yml b/.github/actions/setup-dependencies/linux/action.yml
new file mode 100644
index 000000000..dd0d28364
--- /dev/null
+++ b/.github/actions/setup-dependencies/linux/action.yml
@@ -0,0 +1,26 @@
+name: Setup Linux dependencies
+
+runs:
+ using: composite
+
+ steps:
+ - name: Install host dependencies
+ shell: bash
+ run: |
+ sudo apt-get -y update
+ sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream libxcb-cursor-dev
+
+ - name: Setup AppImage tooling
+ shell: bash
+ run: |
+ declare -A appimage_deps
+ appimage_deps["https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage"]="4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a linuxdeploy-x86_64.AppImage"
+ appimage_deps["https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-x86_64.AppImage"]="15106be885c1c48a021198e7e1e9a48ce9d02a86dd0a1848f00bdbf3c1c92724 linuxdeploy-plugin-qt-x86_64.AppImage"
+ appimage_deps["https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-x86_64.AppImage"]="f1747cf60058e99f1bb9099ee9787d16c10241313b7acec81810ea1b1e568c11 AppImageUpdate-x86_64.AppImage"
+
+ for url in "${!appimage_deps[@]}"; do
+ curl -LO "$url"
+ sha256sum -c - <<< "${appimage_deps[$url]}"
+ done
+
+ sudo apt -y install libopengl0
diff --git a/.github/actions/setup-dependencies/macos/action.yml b/.github/actions/setup-dependencies/macos/action.yml
new file mode 100644
index 000000000..dcbb308c2
--- /dev/null
+++ b/.github/actions/setup-dependencies/macos/action.yml
@@ -0,0 +1,16 @@
+name: Setup macOS dependencies
+
+runs:
+ using: composite
+
+ steps:
+ - name: Install dependencies
+ shell: bash
+ run: |
+ brew update
+ brew install ninja extra-cmake-modules temurin@17
+
+ - name: Set JAVA_HOME
+ shell: bash
+ run: |
+ echo "JAVA_HOME=$(/usr/libexec/java_home -v 17)" >> "$GITHUB_ENV"
diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml
new file mode 100644
index 000000000..0a643f583
--- /dev/null
+++ b/.github/actions/setup-dependencies/windows/action.yml
@@ -0,0 +1,73 @@
+name: Setup Windows Dependencies
+
+inputs:
+ build-type:
+ description: Type for the build
+ required: true
+ default: Debug
+ msystem:
+ description: MSYS2 subsystem to use
+ required: false
+ vcvars-arch:
+ description: Visual Studio architecture to use
+ required: true
+ default: amd64
+
+runs:
+ using: composite
+
+ steps:
+ # NOTE: Installed on MinGW as well for SignTool
+ - name: Enter VS Developer shell
+ if: ${{ runner.os == 'Windows' }}
+ uses: ilammy/msvc-dev-cmd@v1
+ with:
+ arch: ${{ inputs.vcvars-arch }}
+ vsversion: 2022
+
+ - name: Setup MSYS2 (MinGW)
+ if: ${{ inputs.msystem != '' }}
+ uses: msys2/setup-msys2@v2
+ with:
+ msystem: ${{ inputs.msystem }}
+ update: true
+ install: >-
+ git
+ pacboy: >-
+ toolchain:p
+ ccache:p
+ cmake:p
+ extra-cmake-modules:p
+ ninja:p
+ qt6-base:p
+ qt6-svg:p
+ qt6-imageformats:p
+ qt6-5compat:p
+ qt6-networkauth:p
+ cmark:p
+ tomlplusplus:p
+ quazip-qt6:p
+
+ - name: List pacman packages (MinGW)
+ if: ${{ inputs.msystem != '' }}
+ shell: msys2 {0}
+ run: |
+ pacman -Qe
+
+ - name: Retrieve ccache cache (MinGW)
+ if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }}
+ uses: actions/cache@v4.2.3
+ with:
+ path: '${{ github.workspace }}\.ccache'
+ key: ${{ runner.os }}-mingw-w64-ccache-${{ github.run_id }}
+ restore-keys: |
+ ${{ runner.os }}-mingw-w64-ccache
+
+ - name: Setup ccache (MinGW)
+ if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }}
+ shell: msys2 {0}
+ run: |
+ ccache --set-config=cache_dir='${{ github.workspace }}\.ccache'
+ ccache --set-config=max_size='500M'
+ ccache --set-config=compression=true
+ ccache -p # Show config
diff --git a/.github/workflows/blocked-prs.yml b/.github/workflows/blocked-prs.yml
index bd49b7230..ecbaf755d 100644
--- a/.github/workflows/blocked-prs.yml
+++ b/.github/workflows/blocked-prs.yml
@@ -64,7 +64,7 @@ jobs:
"prNumber": .number,
"prHeadSha": .head.sha,
"prHeadLabel": .head.label,
- "prBody": .body,
+ "prBody": (.body // ""),
"prLabels": (reduce .labels[].name as $l ([]; . + [$l]))
}
' <<< "$PR_JSON")"
@@ -125,6 +125,7 @@ jobs:
"type": $type,
"number": .number,
"merged": .merged,
+ "state": (if .state == "open" then "Open" elif .merged then "Merged" else "Closed" end),
"labels": (reduce .labels[].name as $l ([]; . + [$l])),
"basePrUrl": .html_url,
"baseRepoName": .head.repo.name,
@@ -138,11 +139,16 @@ jobs:
)
{
echo "data=$blocked_pr_data";
- echo "all_merged=$(jq -r 'all(.[].merged; .)' <<< "$blocked_pr_data")";
- echo "current_blocking=$(jq -c 'map( select( .merged | not ) | .number )' <<< "$blocked_pr_data" )";
+ echo "all_merged=$(jq -r 'all(.[] | (.type == "Stacked on" and .merged) or (.type == "Blocked on" and (.state != "Open")); .)' <<< "$blocked_pr_data")";
+ echo "current_blocking=$(jq -c 'map(
+ select(
+ (.type == "Stacked on" and (.merged | not)) or
+ (.type == "Blocked on" and (.state == "Open"))
+ ) | .number
+ )' <<< "$blocked_pr_data" )";
} >> "$GITHUB_OUTPUT"
- - name: Add 'blocked' Label is Missing
+ - name: Add 'blocked' Label if Missing
id: label_blocked
if: (fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0) && !contains(fromJSON(env.JOB_DATA).prLabels, 'blocked') && !fromJSON(steps.blocking_data.outputs.all_merged)
continue-on-error: true
@@ -184,14 +190,18 @@ jobs:
# create commit Status, overwrites previous identical context
while read -r pr_data ; do
DESC=$(
- jq -r ' "Blocking PR #" + (.number | tostring) + " is " + (if .merged then "" else "not yet " end) + "merged"' <<< "$pr_data"
+ jq -r 'if .type == "Stacked on" then
+ "Stacked PR #" + (.number | tostring) + " is " + (if .merged then "" else "not yet " end) + "merged"
+ else
+ "Blocking PR #" + (.number | tostring) + " is " + (if .state == "Open" then "" else "not yet " end) + "merged or closed"
+ end ' <<< "$pr_data"
)
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/${OWNER}/${REPO}/statuses/${pr_head_sha}" \
- -f "state=$(jq -r 'if .merged then "success" else "failure" end' <<< "$pr_data")" \
+ -f "state=$(jq -r 'if (.type == "Stacked on" and .merged) or (.type == "Blocked on" and (.state != "Open")) then "success" else "failure" end' <<< "$pr_data")" \
-f "target_url=$(jq -r '.basePrUrl' <<< "$pr_data" )" \
-f "description=$DESC" \
-f "context=ci/blocking-pr-check:$(jq '.number' <<< "$pr_data")"
@@ -214,7 +224,13 @@ jobs:
base_repo_owner=$(jq -r '.baseRepoOwner' <<< "$pr_data")
base_repo_name=$(jq -r '.baseRepoName' <<< "$pr_data")
compare_url="https://github.com/$base_repo_owner/$base_repo_name/compare/$base_ref_name...$pr_head_label"
- status=$(jq -r 'if .merged then ":heavy_check_mark: Merged" else ":x: Not Merged" end' <<< "$pr_data")
+ status=$(jq -r '
+ if .type == "Stacked on" then
+ if .merged then ":heavy_check_mark: Merged" else ":x: Not Merged (" + .state + ")" end
+ else
+ if .state != "Open" then ":white_check_mark: " + .state else ":x: Open" end
+ end
+ ' <<< "$pr_data")
type=$(jq -r '.type' <<< "$pr_data")
echo " - $type #$base_pr $status [(compare)]($compare_url)" >> "$COMMENT_PATH"
done < <(jq -c '.[]' <<< "$BLOCKING_DATA")
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 952b7c515..f4cdae97c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,619 +1,199 @@
name: Build
on:
+ push:
+ branches-ignore:
+ - "renovate/**"
+ paths:
+ # File types
+ - "**.cpp"
+ - "**.h"
+ - "**.java"
+
+ # Directories
+ - "buildconfig/"
+ - "cmake/"
+ - "launcher/"
+ - "libraries/"
+ - "program_info/"
+ - "tests/"
+
+ # Files
+ - "CMakeLists.txt"
+ - "COPYING.md"
+
+ # Workflows
+ - ".github/workflows/build.yml"
+ - ".github/actions/package/"
+ - ".github/actions/setup-dependencies/"
+ pull_request:
+ paths:
+ # File types
+ - "**.cpp"
+ - "**.h"
+
+ # Directories
+ - "buildconfig/"
+ - "cmake/"
+ - "launcher/"
+ - "libraries/"
+ - "program_info/"
+ - "tests/"
+
+ # Files
+ - "CMakeLists.txt"
+ - "COPYING.md"
+
+ # Workflows
+ - ".github/workflows/build.yml"
+ - ".github/actions/package/"
+ - ".github/actions/setup-dependencies/"
workflow_call:
inputs:
- build_type:
- description: Type of build (Debug, Release, RelWithDebInfo, MinSizeRel)
+ build-type:
+ description: Type of build (Debug or Release)
type: string
default: Debug
- is_qt_cached:
- description: Enable Qt caching or not
+ workflow_dispatch:
+ inputs:
+ build-type:
+ description: Type of build (Debug or Release)
type: string
- default: true
- secrets:
- SPARKLE_ED25519_KEY:
- description: Private key for signing Sparkle updates
- required: false
- WINDOWS_CODESIGN_CERT:
- description: Certificate for signing Windows builds
- required: false
- WINDOWS_CODESIGN_PASSWORD:
- description: Password for signing Windows builds
- required: false
- 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
- required: false
- GPG_PRIVATE_KEY_ID:
- description: ID for the GPG_PRIVATE_KEY, to select the signing key
- required: false
+ default: Debug
jobs:
build:
+ name: Build (${{ matrix.artifact-name }})
+
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
- qt_ver: 6
- qt_host: linux
- qt_arch: ""
- qt_version: "6.8.1"
- qt_modules: "qt5compat qtimageformats qtnetworkauth"
- linuxdeploy_hash: "4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a linuxdeploy-x86_64.AppImage"
- linuxdeploy_qt_hash: "15106be885c1c48a021198e7e1e9a48ce9d02a86dd0a1848f00bdbf3c1c92724 linuxdeploy-plugin-qt-x86_64.AppImage"
- appimageupdate_hash: "f1747cf60058e99f1bb9099ee9787d16c10241313b7acec81810ea1b1e568c11 AppImageUpdate-x86_64.AppImage"
+ artifact-name: Linux
+ base-cmake-preset: linux
- os: windows-2022
- name: "Windows-MinGW-w64"
- msystem: clang64
- vcvars_arch: "amd64_x86"
+ artifact-name: Windows-MinGW-w64
+ base-cmake-preset: windows_mingw
+ msystem: CLANG64
+ vcvars-arch: amd64_x86
+
+ - os: windows-11-arm
+ artifact-name: Windows-MinGW-arm64
+ base-cmake-preset: windows_mingw
+ msystem: CLANGARM64
+ vcvars-arch: arm64
- os: windows-2022
- name: "Windows-MSVC"
- msystem: ""
- architecture: "x64"
- vcvars_arch: "amd64"
- qt_ver: 6
- qt_host: "windows"
- qt_arch: "win64_msvc2022_64"
- qt_version: "6.8.1"
- qt_modules: "qt5compat qtimageformats qtnetworkauth"
- nscurl_tag: "v24.9.26.122"
- nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
+ artifact-name: Windows-MSVC
+ base-cmake-preset: windows_msvc
+ # TODO(@getchoo): This is the default in setup-dependencies/windows. Why isn't it working?!?!
+ vcvars-arch: amd64
- os: windows-2022
- name: "Windows-MSVC-arm64"
- msystem: ""
- architecture: "arm64"
- vcvars_arch: "amd64_arm64"
- qt_ver: 6
- qt_host: "windows"
- qt_arch: "win64_msvc2022_arm64_cross_compiled"
- qt_version: "6.8.1"
- qt_modules: "qt5compat qtimageformats qtnetworkauth"
- nscurl_tag: "v24.9.26.122"
- nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
+ artifact-name: Windows-MSVC-arm64
+ base-cmake-preset: windows_msvc_arm64_cross
+ vcvars-arch: amd64_arm64
+ qt-architecture: win64_msvc2022_arm64_cross_compiled
- os: macos-14
- name: macOS
- macosx_deployment_target: 11.0
- qt_ver: 6
- qt_host: mac
- qt_arch: ""
- qt_version: "6.8.1"
- qt_modules: "qt5compat qtimageformats qtnetworkauth"
+ artifact-name: macOS
+ base-cmake-preset: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'macos_universal' || 'macos' }}
+ macosx-deployment-target: 12.0
runs-on: ${{ matrix.os }}
+ defaults:
+ run:
+ shell: ${{ matrix.msystem != '' && 'msys2 {0}' || 'bash' }}
+
env:
- MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- INSTALL_DIR: "install"
- INSTALL_PORTABLE_DIR: "install-portable"
- INSTALL_APPIMAGE_DIR: "install-appdir"
- BUILD_DIR: "build"
- CCACHE_VAR: ""
- HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
+ MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx-deployment-target }}
steps:
##
- # PREPARE
+ # SETUP
##
+
- name: Checkout
uses: actions/checkout@v4
with:
- submodules: "true"
+ submodules: true
- - name: "Setup MSYS2"
- if: runner.os == 'Windows' && matrix.msystem != ''
- uses: msys2/setup-msys2@v2
+ - name: Setup dependencies
+ id: setup-dependencies
+ uses: ./.github/actions/setup-dependencies
with:
+ build-type: ${{ inputs.build-type || 'Debug' }}
msystem: ${{ matrix.msystem }}
- update: true
- install: >-
- git
- mingw-w64-x86_64-binutils
- pacboy: >-
- toolchain:p
- cmake:p
- extra-cmake-modules:p
- ninja:p
- qt6-base:p
- qt6-svg:p
- qt6-imageformats:p
- quazip-qt6:p
- ccache:p
- qt6-5compat:p
- qt6-networkauth:p
- cmark:p
-
- - name: Force newer ccache
- if: runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug'
- run: |
- choco install ccache --version 4.7.1
-
- - name: Setup ccache
- if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug'
- uses: hendrikmuhs/ccache-action@v1.2.18
- with:
- key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
-
- - name: Use ccache on Debug builds only
- if: inputs.build_type == 'Debug'
- shell: bash
- run: |
- echo "CCACHE_VAR=ccache" >> $GITHUB_ENV
-
- - name: Retrieve ccache cache (Windows MinGW-w64)
- if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
- uses: actions/cache@v4.2.3
- with:
- path: '${{ github.workspace }}\.ccache'
- key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
- restore-keys: |
- ${{ matrix.os }}-mingw-w64-ccache
-
- - name: Setup ccache (Windows MinGW-w64)
- if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
- shell: msys2 {0}
- run: |
- ccache --set-config=cache_dir='${{ github.workspace }}\.ccache'
- ccache --set-config=max_size='500M'
- ccache --set-config=compression=true
- ccache -p # Show config
- ccache -z # Zero stats
-
- - name: Configure ccache (Windows MSVC)
- if: ${{ runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug' }}
- run: |
- # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix)
- Copy-Item C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/ccache.exe -Destination C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/cl.exe
- echo "CLToolExe=cl.exe" >> $env:GITHUB_ENV
- echo "CLToolPath=C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/" >> $env:GITHUB_ENV
- echo "TrackFileAccess=false" >> $env:GITHUB_ENV
- # Needed for ccache, but also speeds up compile
- echo "UseMultiToolTask=true" >> $env:GITHUB_ENV
-
- - name: Set short version
- shell: bash
- run: |
- ver_short=`git rev-parse --short HEAD`
- echo "VERSION=$ver_short" >> $GITHUB_ENV
-
- - name: Install Dependencies (Linux)
- if: runner.os == 'Linux'
- run: |
- sudo apt-get -y update
- sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream libxcb-cursor-dev
-
- - name: Install Dependencies (macOS)
- if: runner.os == 'macOS'
- run: |
- brew update
- brew install ninja extra-cmake-modules
-
- - name: Install host Qt (Windows MSVC arm64)
- if: runner.os == 'Windows' && matrix.architecture == 'arm64'
- uses: jurplel/install-qt-action@v4
- with:
- aqtversion: "==3.1.*"
- py7zrversion: ">=0.20.2"
- version: ${{ matrix.qt_version }}
- host: "windows"
- target: "desktop"
- arch: ${{ matrix.qt_arch }}
- modules: ${{ matrix.qt_modules }}
- cache: ${{ inputs.is_qt_cached }}
- cache-key-prefix: host-qt-arm64-windows
- dir: ${{ github.workspace }}\HostQt
- set-env: false
-
- - name: Install Qt (macOS, Linux & Windows MSVC)
- if: matrix.msystem == ''
- uses: jurplel/install-qt-action@v4
- with:
- aqtversion: "==3.1.*"
- py7zrversion: ">=0.20.2"
- version: ${{ matrix.qt_version }}
- target: "desktop"
- arch: ${{ matrix.qt_arch }}
- modules: ${{ matrix.qt_modules }}
- tools: ${{ matrix.qt_tools }}
- cache: ${{ inputs.is_qt_cached }}
-
- - name: Install MSVC (Windows MSVC)
- if: runner.os == 'Windows' # We want this for MinGW builds as well, as we need SignTool
- uses: ilammy/msvc-dev-cmd@v1
- with:
- vsversion: 2022
- arch: ${{ matrix.vcvars_arch }}
-
- - name: Prepare AppImage (Linux)
- if: runner.os == 'Linux'
- env:
- APPIMAGEUPDATE_HASH: ${{ matrix.appimageupdate_hash }}
- LINUXDEPLOY_HASH: ${{ matrix.linuxdeploy_hash }}
- LINUXDEPLOY_QT_HASH: ${{ matrix.linuxdeploy_qt_hash }}
- run: |
- wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage"
- wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-x86_64.AppImage"
-
- wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-x86_64.AppImage"
-
- sha256sum -c - <<< "$LINUXDEPLOY_HASH"
- sha256sum -c - <<< "$LINUXDEPLOY_QT_HASH"
- sha256sum -c - <<< "$APPIMAGEUPDATE_HASH"
-
- sudo apt install libopengl0
-
- - name: Add QT_HOST_PATH var (Windows MSVC arm64)
- if: runner.os == 'Windows' && matrix.architecture == 'arm64'
- run: |
- echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2022_64" >> $env:GITHUB_ENV
-
- - name: Setup java (macOS)
- if: runner.os == 'macOS'
- uses: actions/setup-java@v4
- with:
- distribution: "temurin"
- java-version: "17"
- ##
- # CONFIGURE
- ##
-
- - name: Configure CMake (macOS)
- if: runner.os == 'macOS'
- run: |
- cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=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 }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja
-
- - name: Configure CMake (Windows MinGW-w64)
- if: runner.os == 'Windows' && matrix.msystem != ''
- shell: msys2 {0}
- run: |
- cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja
-
- - name: Configure CMake (Windows MSVC)
- if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture != 'arm64'
- run: |
- cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja
-
- - name: Configure CMake (Windows MSVC arm64)
- if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture == 'arm64'
- run: |
- cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }}
-
- - name: Configure CMake (Linux)
- if: runner.os == 'Linux'
- run: |
- cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=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 }} -G Ninja
+ vcvars-arch: ${{ matrix.vcvars-arch }}
+ qt-architecture: ${{ matrix.qt-architecture }}
##
# BUILD
##
- - name: Build
- if: runner.os != 'Windows'
+ - name: Get CMake preset
+ id: cmake-preset
+ env:
+ BASE_CMAKE_PRESET: ${{ matrix.base-cmake-preset }}
+ PRESET_TYPE: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'debug' || 'ci' }}
run: |
- cmake --build ${{ env.BUILD_DIR }}
+ echo preset="$BASE_CMAKE_PRESET"_"$PRESET_TYPE" >> "$GITHUB_OUTPUT"
- - name: Build (Windows MinGW-w64)
- if: runner.os == 'Windows' && matrix.msystem != ''
- shell: msys2 {0}
+ - name: Run CMake workflow
+ env:
+ CMAKE_PRESET: ${{ steps.cmake-preset.outputs.preset }}
run: |
- cmake --build ${{ env.BUILD_DIR }}
-
- - name: Build (Windows MSVC)
- if: runner.os == 'Windows' && matrix.msystem == ''
- run: |
- cmake --build ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }}
+ cmake --workflow --preset "$CMAKE_PRESET"
##
- # TEST
+ # PACKAGE
##
- - name: Test
- if: runner.os != 'Windows'
+ - name: Get short version
+ id: short-version
+ shell: bash
run: |
- ctest -E "^example64|example$" --test-dir build --output-on-failure
+ echo "version=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
- - name: Test (Windows MinGW-w64)
- if: runner.os == 'Windows' && matrix.msystem != ''
- shell: msys2 {0}
- run: |
- ctest -E "^example64|example$" --test-dir build --output-on-failure
+ - name: Package (Linux)
+ if: ${{ runner.os == 'Linux' }}
+ uses: ./.github/actions/package/linux
+ with:
+ version: ${{ steps.short-version.outputs.version }}
+ build-type: ${{ steps.setup-dependencies.outputs.build-type }}
+ cmake-preset: ${{ steps.cmake-preset.outputs.preset }}
+ qt-version: ${{ steps.setup-dependencies.outputs.qt-version }}
- - name: Test (Windows MSVC)
- if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture != 'arm64'
- run: |
- ctest -E "^example64|example$" --test-dir build --output-on-failure -C ${{ inputs.build_type }}
-
- ##
- # 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
+ gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
+ gpg-private-key-id: ${{ secrets.GPG_PRIVATE_KEY_ID }}
- name: Package (macOS)
- if: runner.os == 'macOS'
- run: |
- cmake --install ${{ env.BUILD_DIR }}
-
- cd ${{ env.INSTALL_DIR }}
- chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher"
-
- if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then
- APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}'
- ENTITLEMENTS_FILE='../program_info/App.entitlements'
- else
- APPLE_CODESIGN_ID='-'
- ENTITLEMENTS_FILE='../program_info/AdhocSignedApp.entitlements'
- fi
-
- sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "$ENTITLEMENTS_FILE" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
- mv "PrismLauncher.app" "Prism Launcher.app"
-
- - 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'
- run: |
- if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then
- echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem
- signature=$(/opt/homebrew/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:
- - :memo: Sparkle Signature (ed25519): \`$signature\`
- EOF
- else
- cat >> $GITHUB_STEP_SUMMARY << EOF
- ### Artifact Information :information_source:
- - :warning: Sparkle Signature (ed25519): No private key available (likely a pull request or fork)
- EOF
- fi
-
- - name: Package (Windows MinGW-w64)
- if: runner.os == 'Windows' && matrix.msystem != ''
- shell: msys2 {0}
- run: |
- cmake --install ${{ env.BUILD_DIR }}
- touch ${{ env.INSTALL_DIR }}/manifest.txt
- for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt
-
- - name: Package (Windows MSVC)
- if: runner.os == 'Windows' && matrix.msystem == ''
- run: |
- cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }}
-
- 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
-
- - name: Fetch codesign certificate (Windows)
- if: runner.os == 'Windows'
- shell: bash # yes, we are not using MSYS2 or PowerShell here
- run: |
- echo '${{ secrets.WINDOWS_CODESIGN_CERT }}' | base64 --decode > codesign.pfx
-
- - name: Sign executable (Windows)
- if: runner.os == 'Windows'
- run: |
- if (Get-Content ./codesign.pfx){
- cd ${{ env.INSTALL_DIR }}
- # We ship the exact same executable for portable and non-portable editions, so signing just once is fine
- SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_updater.exe prismlauncher_filelink.exe
- } else {
- ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
- }
-
- - name: Package (Windows MinGW-w64, portable)
- if: runner.os == 'Windows' && matrix.msystem != ''
- shell: msys2 {0}
- run: |
- cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
- cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
- for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
-
- - name: Package (Windows MSVC, portable)
- if: runner.os == 'Windows' && matrix.msystem == ''
- run: |
- cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
- cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
-
- Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
-
- - name: Package (Windows, installer)
- if: runner.os == 'Windows'
- run: |
- if ('${{ matrix.nscurl_tag }}') {
- New-Item -Name NSISPlugins -ItemType Directory
- Invoke-Webrequest https://github.com/negrutiu/nsis-nscurl/releases/download/${{ matrix.nscurl_tag }}/NScurl.zip -OutFile NSISPlugins\NScurl.zip
- $nscurl_hash = Get-FileHash NSISPlugins\NScurl.zip -Algorithm Sha256 | Select-Object -ExpandProperty Hash
- if ( $nscurl_hash -ne "${{ matrix.nscurl_sha256 }}") {
- echo "::error:: NSCurl.zip sha256 mismatch"
- exit 1
- }
- Expand-Archive -Path NSISPlugins\NScurl.zip -DestinationPath NSISPlugins\NScurl
- }
- cd ${{ env.INSTALL_DIR }}
- makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi"
-
- - name: Sign installer (Windows)
- if: runner.os == 'Windows'
- run: |
- if (Get-Content ./codesign.pfx){
- SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe
- } else {
- ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
- }
-
- - name: Package AppImage (Linux)
- if: runner.os == 'Linux'
- shell: bash
- env:
- GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
- run: |
- cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr
-
- mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml
- export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated
-
- export OUTPUT="PrismLauncher-Linux-x86_64.AppImage"
-
- chmod +x linuxdeploy-*.AppImage
-
- mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib
- mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
-
- cp -r ${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
-
- cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
- cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
- cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
-
- LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
- export LD_LIBRARY_PATH
-
- chmod +x AppImageUpdate-x86_64.AppImage
- cp AppImageUpdate-x86_64.AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin
-
- export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-x86_64.AppImage.zsync"
-
- if [ '${{ secrets.GPG_PRIVATE_KEY_ID }}' != '' ]; then
- export SIGN=1
- export SIGN_KEY=${{ secrets.GPG_PRIVATE_KEY_ID }}
- mkdir -p ~/.gnupg/
- echo "$GPG_PRIVATE_KEY" > ~/.gnupg/private.key
- gpg --import ~/.gnupg/private.key
- else
- echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY
- fi
-
- ./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.prismlauncher.PrismLauncher.svg
-
- 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.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
- cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
- cp /usr/lib/x86_64-linux-gnu/libffi.so.*.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
- mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib
-
- for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
- cd ${{ env.INSTALL_PORTABLE_DIR }}
- tar -czf ../PrismLauncher-portable.tar.gz *
-
- ##
- # UPLOAD BUILDS
- ##
-
- - name: Upload binary tarball (macOS)
- if: runner.os == 'macOS'
- uses: actions/upload-artifact@v4
+ if: ${{ runner.os == 'macOS' }}
+ uses: ./.github/actions/package/macos
with:
- name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
- path: PrismLauncher.zip
+ version: ${{ steps.short-version.outputs.version }}
+ build-type: ${{ steps.setup-dependencies.outputs.build-type }}
+ artifact-name: ${{ matrix.artifact-name }}
- - name: Upload binary zip (Windows)
- if: runner.os == 'Windows'
- uses: actions/upload-artifact@v4
+ 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 }}
+ sparkle-ed25519-key: ${{ secrets.SPARKLE-ED25519_KEY }}
+
+ - name: Package (Windows)
+ if: ${{ runner.os == 'Windows' }}
+ uses: ./.github/actions/package/windows
with:
- name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
- path: ${{ env.INSTALL_DIR }}/**
+ version: ${{ steps.short-version.outputs.version }}
+ build-type: ${{ steps.setup-dependencies.outputs.build-type }}
+ artifact-name: ${{ matrix.artifact-name }}
+ msystem: ${{ matrix.msystem }}
- - name: Upload binary zip (Windows, portable)
- if: runner.os == 'Windows'
- uses: actions/upload-artifact@v4
- with:
- name: PrismLauncher-${{ matrix.name }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
- path: ${{ env.INSTALL_PORTABLE_DIR }}/**
-
- - name: Upload installer (Windows)
- if: runner.os == 'Windows'
- uses: actions/upload-artifact@v4
- with:
- name: PrismLauncher-${{ matrix.name }}-Setup-${{ env.VERSION }}-${{ inputs.build_type }}
- path: PrismLauncher-Setup.exe
-
- - name: Upload binary tarball (Linux, portable)
- if: runner.os == 'Linux'
- uses: actions/upload-artifact@v4
- with:
- name: PrismLauncher-${{ runner.os }}-Qt6-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
- path: PrismLauncher-portable.tar.gz
-
- - name: Upload AppImage (Linux)
- if: runner.os == 'Linux'
- uses: actions/upload-artifact@v4
- with:
- name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
- path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
-
- - name: Upload AppImage Zsync (Linux)
- if: runner.os == 'Linux'
- uses: actions/upload-artifact@v4
- with:
- name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage.zsync
- path: PrismLauncher-Linux-x86_64.AppImage.zsync
-
- - name: ccache stats (Windows MinGW-w64)
- if: runner.os == 'Windows' && matrix.msystem != ''
- shell: msys2 {0}
- run: |
- ccache -s
+ windows-codesign-cert: ${{ secrets.WINDOWS_CODESIGN_CERT }}
+ windows-codesign-password: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index a5ac537f1..f8fae8ecf 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -2,37 +2,50 @@ name: "CodeQL Code Scanning"
on:
push:
- # NOTE: `!` doesn't work with `paths-ignore` :(
- # So we a catch-all glob instead
- # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674
paths:
- - "**"
- - "!.github/**"
- - ".github/workflows/codeql.yml"
- - "!flatpak/"
- - "!nix/"
- - "!scripts/"
+ # File types
+ - "**.cpp"
+ - "**.h"
+ - "**.java"
- - "!.git*"
- - "!.envrc"
- - "!**.md"
+ # Directories
+ - "buildconfig/"
+ - "cmake/"
+ - "launcher/"
+ - "libraries/"
+ - "program_info/"
+ - "tests/"
+
+ # Files
+ - "CMakeLists.txt"
- "COPYING.md"
- - "!renovate.json"
+
+ # Workflows
+ - ".github/codeql"
+ - ".github/workflows/codeql.yml"
+ - ".github/actions/setup-dependencies/"
pull_request:
- # See above
paths:
- - "**"
- - "!.github/**"
- - ".github/workflows/codeql.yml"
- - "!flatpak/"
- - "!nix/"
- - "!scripts/"
+ # File types
+ - "**.cpp"
+ - "**.h"
- - "!.git*"
- - "!.envrc"
- - "!**.md"
+ # Directories
+ - "buildconfig/"
+ - "cmake/"
+ - "launcher/"
+ - "libraries/"
+ - "program_info/"
+ - "tests/"
+
+ # Files
+ - "CMakeLists.txt"
- "COPYING.md"
- - "!renovate.json"
+
+ # Workflows
+ - ".github/codeql"
+ - ".github/workflows/codeql.yml"
+ - ".github/actions/setup-dependencies/"
workflow_dispatch:
jobs:
@@ -52,28 +65,15 @@ jobs:
queries: security-and-quality
languages: cpp, java
- - name: Install Dependencies
- run: sudo apt-get -y update
-
- sudo apt-get -y install ninja-build extra-cmake-modules scdoc
-
- - name: Install Qt
- uses: jurplel/install-qt-action@v3
+ - name: Setup dependencies
+ uses: ./.github/actions/setup-dependencies
with:
- aqtversion: "==3.1.*"
- py7zrversion: ">=0.20.2"
- version: "6.8.1"
- host: "linux"
- target: "desktop"
- arch: ""
- modules: "qt5compat qtimageformats qtnetworkauth"
- tools: ""
+ build-type: Debug
- name: Configure and Build
run: |
- cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr -G Ninja
-
- cmake --build build
+ cmake --preset linux_debug
+ cmake --build --preset linux_debug
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml
index 8caba46fa..cab0edeb7 100644
--- a/.github/workflows/flatpak.yml
+++ b/.github/workflows/flatpak.yml
@@ -5,35 +5,52 @@ on:
# We don't do anything with these artifacts on releases. They go to Flathub
tags-ignore:
- "*"
- # NOTE: `!` doesn't work with `paths-ignore` :(
- # So we a catch-all glob instead
- # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674
paths:
- - "**"
- - "!.github/**"
- - ".github/workflows/flatpak.yml"
- - "!nix/"
- - "!scripts/"
+ # File types
+ - "**.cpp"
+ - "**.h"
+ - "**.java"
- - "!.git*"
- - "!.envrc"
- - "!**.md"
+ # Build files
+ - "flatpak/"
+
+ # Directories
+ - "buildconfig/"
+ - "cmake/"
+ - "launcher/"
+ - "libraries/"
+ - "program_info/"
+ - "tests/"
+
+ # Files
+ - "CMakeLists.txt"
- "COPYING.md"
- - "!renovate.json"
+
+ # Workflows
+ - ".github/workflows/flatpak.yml"
pull_request:
- # See above
paths:
- - "**"
- - "!.github/**"
- - ".github/workflows/flatpak.yml"
- - "!nix/"
- - "!scripts/"
+ # File types
+ - "**.cpp"
+ - "**.h"
- - "!.git*"
- - "!.envrc"
- - "!**.md"
+ # Build files
+ - "flatpak/"
+
+ # Directories
+ - "buildconfig/"
+ - "cmake/"
+ - "launcher/"
+ - "libraries/"
+ - "program_info/"
+ - "tests/"
+
+ # Files
+ - "CMakeLists.txt"
- "COPYING.md"
- - "!renovate.json"
+
+ # Workflows
+ - ".github/workflows/flatpak.yml"
workflow_dispatch:
permissions:
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
index 75ef7c65a..80b41161a 100644
--- a/.github/workflows/nix.yml
+++ b/.github/workflows/nix.yml
@@ -4,34 +4,56 @@ on:
push:
tags:
- "*"
- # NOTE: `!` doesn't work with `paths-ignore` :(
- # So we a catch-all glob instead
- # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674
paths:
- - "**"
- - "!.github/**"
- - ".github/workflows/nix.yml"
- - "!flatpak/"
- - "!scripts/"
+ # File types
+ - "**.cpp"
+ - "**.h"
+ - "**.java"
- - "!.git*"
- - "!.envrc"
- - "!**.md"
+ # Build files
+ - "**.nix"
+ - "nix/"
+ - "flake.lock"
+
+ # Directories
+ - "buildconfig/"
+ - "cmake/"
+ - "launcher/"
+ - "libraries/"
+ - "program_info/"
+ - "tests/"
+
+ # Files
+ - "CMakeLists.txt"
- "COPYING.md"
- - "!renovate.json"
+
+ # Workflows
+ - ".github/workflows/nix.yml"
pull_request_target:
paths:
- - "**"
- - "!.github/**"
- - ".github/workflows/nix.yml"
- - "!flatpak/"
- - "!scripts/"
+ # File types
+ - "**.cpp"
+ - "**.h"
- - "!.git*"
- - "!.envrc"
- - "!**.md"
+ # Build files
+ - "**.nix"
+ - "nix/"
+ - "flake.lock"
+
+ # Directories
+ - "buildconfig/"
+ - "cmake/"
+ - "launcher/"
+ - "libraries/"
+ - "program_info/"
+ - "tests/"
+
+ # Files
+ - "CMakeLists.txt"
- "COPYING.md"
- - "!renovate.json"
+
+ # Workflows
+ - ".github/workflows/nix.yml"
workflow_dispatch:
permissions:
@@ -89,7 +111,7 @@ jobs:
# For PRs
- name: Setup Nix Magic Cache
if: ${{ env.USE_DETERMINATE == 'true' }}
- uses: DeterminateSystems/flakehub-cache-action@v1
+ uses: DeterminateSystems/flakehub-cache-action@v2
# For in-tree builds
- name: Setup Cachix
diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/release.yml
similarity index 83%
rename from .github/workflows/trigger_release.yml
rename to .github/workflows/release.yml
index 96f616a43..6e879cfd7 100644
--- a/.github/workflows/trigger_release.yml
+++ b/.github/workflows/release.yml
@@ -10,20 +10,8 @@ jobs:
name: Build Release
uses: ./.github/workflows/build.yml
with:
- build_type: Release
- is_qt_cached: false
- secrets:
- SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
- WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
- WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
- 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 }}
+ build-type: Release
+ secrets: inherit
create_release:
needs: build_release
@@ -78,6 +66,17 @@ jobs:
cd ..
done
+ for d in PrismLauncher-Windows-MinGW-arm64*; do
+ cd "${d}" || continue
+ INST="$(echo -n ${d} | grep -o Setup || true)"
+ PORT="$(echo -n ${d} | grep -o Portable || true)"
+ NAME="PrismLauncher-Windows-MinGW-arm64"
+ test -z "${PORT}" || NAME="${NAME}-Portable"
+ test -z "${INST}" || mv PrismLauncher-*.exe ../${NAME}-Setup-${{ env.VERSION }}.exe
+ test -n "${INST}" || zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" *
+ cd ..
+ done
+
- name: Create release
id: create_release
uses: softprops/action-gh-release@v2
@@ -94,6 +93,9 @@ jobs:
PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip
PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MinGW-w64-Setup-${{ env.VERSION }}.exe
+ PrismLauncher-Windows-MinGW-arm64-${{ env.VERSION }}.zip
+ PrismLauncher-Windows-MinGW-arm64-Portable-${{ env.VERSION }}.zip
+ PrismLauncher-Windows-MinGW-arm64-Setup-${{ env.VERSION }}.exe
PrismLauncher-Windows-MSVC-arm64-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-arm64-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-arm64-Setup-${{ env.VERSION }}.exe
diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml
deleted file mode 100644
index e4c90ef0b..000000000
--- a/.github/workflows/trigger_builds.yml
+++ /dev/null
@@ -1,60 +0,0 @@
-name: Build Application
-
-on:
- push:
- branches-ignore:
- - "renovate/**"
- # NOTE: `!` doesn't work with `paths-ignore` :(
- # So we a catch-all glob instead
- # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674
- paths:
- - "**"
- - "!.github/**"
- - ".github/workflows/build.yml"
- - ".github/workflows/trigger_builds.yml"
- - "!flatpak/"
- - "!nix/"
- - "!scripts/"
-
- - "!.git*"
- - "!.envrc"
- - "!**.md"
- - "COPYING.md"
- - "!renovate.json"
- pull_request:
- # See above
- paths:
- - "**"
- - "!.github/**"
- - ".github/workflows/build.yml"
- - ".github/workflows/trigger_builds.yml"
- - "!flatpak/"
- - "!nix/"
- - "!scripts/"
-
- - "!.git*"
- - "!.envrc"
- - "!**.md"
- - "COPYING.md"
- - "!renovate.json"
- workflow_dispatch:
-
-jobs:
- build_debug:
- name: Build Debug
- uses: ./.github/workflows/build.yml
- with:
- build_type: Debug
- is_qt_cached: true
- secrets:
- SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
- WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
- WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
- 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/update-flake.yml b/.github/workflows/update-flake.yml
index f4b1c4f5d..7480ba46e 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@754537aaedb35f72ab11a60cc162c49ef3016495 # v31
+ - uses: cachix/install-nix-action@17fe5fb4a23ad6cbbe47d6b3f359611ad276644c # v31
- - uses: DeterminateSystems/update-flake-lock@v24
+ - uses: DeterminateSystems/update-flake-lock@v25
with:
commit-msg: "chore(nix): update lockfile"
pr-title: "chore(nix): update lockfile"
diff --git a/.gitignore b/.gitignore
index b563afbc7..00afabbfa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ CMakeLists.txt.user.*
CMakeSettings.json
/CMakeFiles
CMakeCache.txt
+CMakeUserPresets.json
/.project
/.settings
/.idea
diff --git a/.gitmodules b/.gitmodules
index 0c56d8768..0a0a50bee 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -19,6 +19,6 @@
[submodule "flatpak/shared-modules"]
path = flatpak/shared-modules
url = https://github.com/flathub/shared-modules.git
-[submodule "libraries/qt-qrcodegenerator/QR-Code-generator"]
- path = libraries/qt-qrcodegenerator/QR-Code-generator
+[submodule "libraries/qrcodegenerator"]
+ path = libraries/qrcodegenerator
url = https://github.com/nayuki/QR-Code-generator
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 68d900c27..ce3d433fb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -475,7 +475,6 @@ add_subdirectory(libraries/libnbtplusplus)
add_subdirectory(libraries/systeminfo) # system information library
add_subdirectory(libraries/launcher) # java based launcher part for Minecraft
add_subdirectory(libraries/javacheck) # java compatibility checker
-add_subdirectory(libraries/qt-qrcodegenerator) # qr code generator
if(FORCE_BUNDLED_ZLIB)
message(STATUS "Using bundled zlib")
@@ -533,6 +532,15 @@ add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
add_subdirectory(libraries/qdcss) # css parser
+# qr code generator
+set(QRCODE_SOURCES
+ libraries/qrcodegenerator/cpp/qrcodegen.cpp
+ libraries/qrcodegenerator/cpp/qrcodegen.hpp
+)
+add_library(qrcodegenerator STATIC ${QRCODE_SOURCES})
+target_include_directories(qrcodegenerator PUBLIC "libraries/qrcodegenerator/cpp/" )
+generate_export_header(qrcodegenerator)
+
############################### Built Artifacts ###############################
add_subdirectory(buildconfig)
diff --git a/CMakePresets.json b/CMakePresets.json
new file mode 100644
index 000000000..f8e688b89
--- /dev/null
+++ b/CMakePresets.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
+ "version": 8,
+ "cmakeMinimumRequired": {
+ "major": 3,
+ "minor": 28
+ },
+ "include": [
+ "cmake/linuxPreset.json",
+ "cmake/macosPreset.json",
+ "cmake/windowsMinGWPreset.json",
+ "cmake/windowsMSVCPreset.json"
+ ]
+}
diff --git a/COPYING.md b/COPYING.md
index 1ebde116f..f9b905351 100644
--- a/COPYING.md
+++ b/COPYING.md
@@ -404,7 +404,7 @@
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see .
-## qt-qrcodegenerator (`libraries/qt-qrcodegenerator`)
+## QR-Code-generator (`libraries/qrcodegenerator`)
Copyright © 2024 Project Nayuki. (MIT License)
https://www.nayuki.io/page/qr-code-generator-library
diff --git a/README.md b/README.md
index 9c4909509..361864dfe 100644
--- a/README.md
+++ b/README.md
@@ -76,7 +76,13 @@ We thank all the wonderful backers over at Open Collective! Support Prism Launch
Thanks to JetBrains for providing us a few licenses for all their products, as part of their [Open Source program](https://www.jetbrains.com/opensource/).
-[](https://www.jetbrains.com/opensource/)
+
+
+
+
+
+
+
Thanks to Weblate for hosting our translation efforts.
diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in
index 6bebcb80e..3637e7369 100644
--- a/buildconfig/BuildConfig.cpp.in
+++ b/buildconfig/BuildConfig.cpp.in
@@ -53,7 +53,6 @@ Config::Config()
LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@";
USER_AGENT = "@Launcher_UserAgent@";
- USER_AGENT_UNCACHED = USER_AGENT + " (Uncached)";
// Version information
VERSION_MAJOR = @Launcher_VERSION_MAJOR@;
diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h
index b59adcb57..10c38e3d6 100644
--- a/buildconfig/BuildConfig.h
+++ b/buildconfig/BuildConfig.h
@@ -107,9 +107,6 @@ class Config {
/// User-Agent to use.
QString USER_AGENT;
- /// User-Agent to use for uncached requests.
- QString USER_AGENT_UNCACHED;
-
/// The git commit hash of this build
QString GIT_COMMIT;
diff --git a/cmake/commonPresets.json b/cmake/commonPresets.json
new file mode 100644
index 000000000..9cdf51649
--- /dev/null
+++ b/cmake/commonPresets.json
@@ -0,0 +1,81 @@
+{
+ "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
+ "version": 8,
+ "configurePresets": [
+ {
+ "name": "base",
+ "hidden": true,
+ "binaryDir": "build",
+ "installDir": "install",
+ "cacheVariables": {
+ "Launcher_BUILD_PLATFORM": "custom"
+ }
+ },
+ {
+ "name": "base_debug",
+ "hidden": true,
+ "inherits": [
+ "base"
+ ],
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Debug"
+ }
+ },
+ {
+ "name": "base_release",
+ "hidden": true,
+ "inherits": [
+ "base"
+ ],
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Release",
+ "ENABLE_LTO": "ON"
+ }
+ },
+ {
+ "name": "base_ci",
+ "hidden": true,
+ "inherits": [
+ "base_release"
+ ],
+ "cacheVariables": {
+ "Launcher_BUILD_PLATFORM": "official",
+ "Launcher_FORCE_BUNDLED_LIBS": "ON"
+ }
+ }
+ ],
+ "testPresets": [
+ {
+ "name": "base",
+ "hidden": true,
+ "output": {
+ "outputOnFailure": true
+ },
+ "execution": {
+ "noTestsAction": "error"
+ },
+ "filter": {
+ "exclude": {
+ "name": "^example64|example$"
+ }
+ }
+ },
+ {
+ "name": "base_debug",
+ "hidden": true,
+ "inherits": [
+ "base"
+ ],
+ "output": {
+ "debug": true
+ }
+ },
+ {
+ "name": "base_release",
+ "hidden": true,
+ "inherits": [
+ "base"
+ ]
+ }
+ ]
+}
diff --git a/cmake/linuxPreset.json b/cmake/linuxPreset.json
new file mode 100644
index 000000000..b8bfe4ff0
--- /dev/null
+++ b/cmake/linuxPreset.json
@@ -0,0 +1,180 @@
+{
+ "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
+ "version": 8,
+ "include": [
+ "commonPresets.json"
+ ],
+ "configurePresets": [
+ {
+ "name": "linux_base",
+ "hidden": true,
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Linux"
+ },
+ "generator": "Ninja",
+ "cacheVariables": {
+ "Launcher_BUILD_ARTIFACT": "Linux-Qt6",
+ "Launcher_ENABLE_JAVA_DOWNLOADER": "ON"
+ }
+ },
+ {
+ "name": "linux_debug",
+ "inherits": [
+ "base_debug",
+ "linux_base"
+ ],
+ "displayName": "Linux (Debug)"
+ },
+ {
+ "name": "linux_release",
+ "inherits": [
+ "base_release",
+ "linux_base"
+ ],
+ "displayName": "Linux (Release)"
+ },
+ {
+ "name": "linux_ci",
+ "inherits": [
+ "base_ci",
+ "linux_base"
+ ],
+ "displayName": "Linux (CI)",
+ "cacheVariables": {
+ "Launcher_BUILD_ARTIFACT": "Linux-Qt6"
+ },
+ "installDir": "/usr"
+ }
+ ],
+ "buildPresets": [
+ {
+ "name": "linux_base",
+ "hidden": true,
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Linux"
+ }
+ },
+ {
+ "name": "linux_debug",
+ "inherits": [
+ "linux_base"
+ ],
+ "displayName": "Linux (Debug)",
+ "configurePreset": "linux_debug"
+ },
+ {
+ "name": "linux_release",
+ "inherits": [
+ "linux_base"
+ ],
+ "displayName": "Linux (Release)",
+ "configurePreset": "linux_release"
+ },
+ {
+ "name": "linux_ci",
+ "inherits": [
+ "linux_base"
+ ],
+ "displayName": "Linux (CI)",
+ "configurePreset": "linux_ci"
+ }
+ ],
+ "testPresets": [
+ {
+ "name": "linux_base",
+ "hidden": true,
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Linux"
+ }
+ },
+ {
+ "name": "linux_debug",
+ "inherits": [
+ "base_debug",
+ "linux_base"
+ ],
+ "displayName": "Linux (Debug)",
+ "configurePreset": "linux_debug"
+ },
+ {
+ "name": "linux_release",
+ "inherits": [
+ "base_release",
+ "linux_base"
+ ],
+ "displayName": "Linux (Release)",
+ "configurePreset": "linux_release"
+ },
+ {
+ "name": "linux_ci",
+ "inherits": [
+ "base_release",
+ "linux_base"
+ ],
+ "displayName": "Linux (CI)",
+ "configurePreset": "linux_ci"
+ }
+ ],
+ "workflowPresets": [
+ {
+ "name": "linux_debug",
+ "displayName": "Linux (Debug)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "linux_debug"
+ },
+ {
+ "type": "build",
+ "name": "linux_debug"
+ },
+ {
+ "type": "test",
+ "name": "linux_debug"
+ }
+ ]
+ },
+ {
+ "name": "linux",
+ "displayName": "Linux (Release)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "linux_release"
+ },
+ {
+ "type": "build",
+ "name": "linux_release"
+ },
+ {
+ "type": "test",
+ "name": "linux_release"
+ }
+ ]
+ },
+ {
+ "name": "linux_ci",
+ "displayName": "Linux (CI)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "linux_ci"
+ },
+ {
+ "type": "build",
+ "name": "linux_ci"
+ },
+ {
+ "type": "test",
+ "name": "linux_ci"
+ }
+ ]
+ }
+ ]
+}
diff --git a/cmake/macosPreset.json b/cmake/macosPreset.json
new file mode 100644
index 000000000..726949934
--- /dev/null
+++ b/cmake/macosPreset.json
@@ -0,0 +1,272 @@
+{
+ "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
+ "version": 8,
+ "include": [
+ "commonPresets.json"
+ ],
+ "configurePresets": [
+ {
+ "name": "macos_base",
+ "hidden": true,
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Darwin"
+ },
+ "generator": "Ninja"
+ },
+ {
+ "name": "macos_universal_base",
+ "hidden": true,
+ "inherits": [
+ "macos_base"
+ ],
+ "cacheVariables": {
+ "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64",
+ "Launcher_BUILD_ARTIFACT": "macOS-Qt6"
+ }
+ },
+ {
+ "name": "macos_debug",
+ "inherits": [
+ "base_debug",
+ "macos_base"
+ ],
+ "displayName": "macOS (Debug)"
+ },
+ {
+ "name": "macos_release",
+ "inherits": [
+ "base_release",
+ "macos_base"
+ ],
+ "displayName": "macOS (Release)"
+ },
+ {
+ "name": "macos_universal_debug",
+ "inherits": [
+ "base_debug",
+ "macos_universal_base"
+ ],
+ "displayName": "macOS (Universal Binary, Debug)"
+ },
+ {
+ "name": "macos_universal_release",
+ "inherits": [
+ "base_release",
+ "macos_universal_base"
+ ],
+ "displayName": "macOS (Universal Binary, Release)"
+ },
+ {
+ "name": "macos_ci",
+ "inherits": [
+ "base_ci",
+ "macos_universal_base"
+ ],
+ "displayName": "macOS (CI)",
+ "cacheVariables": {
+ "Launcher_BUILD_ARTIFACT": "macOS-Qt6"
+ }
+ }
+ ],
+ "buildPresets": [
+ {
+ "name": "macos_base",
+ "hidden": true,
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Darwin"
+ }
+ },
+ {
+ "name": "macos_debug",
+ "inherits": [
+ "macos_base"
+ ],
+ "displayName": "macOS (Debug)",
+ "configurePreset": "macos_debug"
+ },
+ {
+ "name": "macos_release",
+ "inherits": [
+ "macos_base"
+ ],
+ "displayName": "macOS (Release)",
+ "configurePreset": "macos_release"
+ },
+ {
+ "name": "macos_universal_debug",
+ "inherits": [
+ "macos_base"
+ ],
+ "displayName": "macOS (Universal Binary, Debug)",
+ "configurePreset": "macos_universal_debug"
+ },
+ {
+ "name": "macos_universal_release",
+ "inherits": [
+ "macos_base"
+ ],
+ "displayName": "macOS (Universal Binary, Release)",
+ "configurePreset": "macos_universal_release"
+ },
+ {
+ "name": "macos_ci",
+ "inherits": [
+ "macos_base"
+ ],
+ "displayName": "macOS (CI)",
+ "configurePreset": "macos_ci"
+ }
+ ],
+ "testPresets": [
+ {
+ "name": "macos_base",
+ "hidden": true,
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Darwin"
+ }
+ },
+ {
+ "name": "macos_debug",
+ "inherits": [
+ "base_debug",
+ "macos_base"
+ ],
+ "displayName": "MacOS (Debug)",
+ "configurePreset": "macos_debug"
+ },
+ {
+ "name": "macos_release",
+ "inherits": [
+ "base_release",
+ "macos_base"
+ ],
+ "displayName": "macOS (Release)",
+ "configurePreset": "macos_release"
+ },
+ {
+ "name": "macos_universal_debug",
+ "inherits": [
+ "base_debug",
+ "macos_base"
+ ],
+ "displayName": "MacOS (Universal Binary, Debug)",
+ "configurePreset": "macos_universal_debug"
+ },
+ {
+ "name": "macos_universal_release",
+ "inherits": [
+ "base_release",
+ "macos_base"
+ ],
+ "displayName": "macOS (Universal Binary, Release)",
+ "configurePreset": "macos_universal_release"
+ },
+ {
+ "name": "macos_ci",
+ "inherits": [
+ "base_release",
+ "macos_base"
+ ],
+ "displayName": "macOS (CI)",
+ "configurePreset": "macos_ci"
+ }
+ ],
+ "workflowPresets": [
+ {
+ "name": "macos_debug",
+ "displayName": "macOS (Debug)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "macos_debug"
+ },
+ {
+ "type": "build",
+ "name": "macos_debug"
+ },
+ {
+ "type": "test",
+ "name": "macos_debug"
+ }
+ ]
+ },
+ {
+ "name": "macos",
+ "displayName": "macOS (Release)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "macos_release"
+ },
+ {
+ "type": "build",
+ "name": "macos_release"
+ },
+ {
+ "type": "test",
+ "name": "macos_release"
+ }
+ ]
+ },
+ {
+ "name": "macos_universal_debug",
+ "displayName": "macOS (Universal Binary, Debug)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "macos_universal_debug"
+ },
+ {
+ "type": "build",
+ "name": "macos_universal_debug"
+ },
+ {
+ "type": "test",
+ "name": "macos_universal_debug"
+ }
+ ]
+ },
+ {
+ "name": "macos_universal",
+ "displayName": "macOS (Universal Binary, Release)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "macos_universal_release"
+ },
+ {
+ "type": "build",
+ "name": "macos_universal_release"
+ },
+ {
+ "type": "test",
+ "name": "macos_universal_release"
+ }
+ ]
+ },
+ {
+ "name": "macos_ci",
+ "displayName": "macOS (CI)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "macos_ci"
+ },
+ {
+ "type": "build",
+ "name": "macos_ci"
+ },
+ {
+ "type": "test",
+ "name": "macos_ci"
+ }
+ ]
+ }
+ ]
+}
diff --git a/cmake/windowsMSVCPreset.json b/cmake/windowsMSVCPreset.json
new file mode 100644
index 000000000..eb6a38b19
--- /dev/null
+++ b/cmake/windowsMSVCPreset.json
@@ -0,0 +1,311 @@
+{
+ "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
+ "version": 8,
+ "include": [
+ "commonPresets.json"
+ ],
+ "configurePresets": [
+ {
+ "name": "windows_msvc_base",
+ "hidden": true,
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Windows"
+ },
+ "cacheVariables": {
+ "Launcher_BUILD_ARTIFACT": "Windows-MSVC-Qt6"
+ }
+ },
+ {
+ "name": "windows_msvc_arm64_cross_base",
+ "hidden": true,
+ "inherits": [
+ "windows_msvc_base"
+ ],
+ "architecture": "arm64",
+ "cacheVariables": {
+ "Launcher_BUILD_ARTIFACT": "Windows-MSVC-arm64-Qt6"
+ }
+ },
+ {
+ "name": "windows_msvc_debug",
+ "inherits": [
+ "base_debug",
+ "windows_msvc_base"
+ ],
+ "displayName": "Windows MSVC (Debug)",
+ "generator": "Ninja"
+ },
+ {
+ "name": "windows_msvc_release",
+ "inherits": [
+ "base_release",
+ "windows_msvc_base"
+ ],
+ "displayName": "Windows MSVC (Release)"
+ },
+ {
+ "name": "windows_msvc_arm64_cross_debug",
+ "inherits": [
+ "base_debug",
+ "windows_msvc_arm64_cross_base"
+ ],
+ "displayName": "Windows MSVC (ARM64 cross, Debug)"
+ },
+ {
+ "name": "windows_msvc_arm64_cross_release",
+ "inherits": [
+ "base_release",
+ "windows_msvc_arm64_cross_base"
+ ],
+ "displayName": "Windows MSVC (ARM64 cross, Release)"
+ },
+ {
+ "name": "windows_msvc_ci",
+ "inherits": [
+ "base_ci",
+ "windows_msvc_base"
+ ],
+ "displayName": "Windows MSVC (CI)",
+ "cacheVariables": {
+ "Launcher_BUILD_ARTIFACT": "Windows-MSVC-Qt6"
+ }
+ },
+ {
+ "name": "windows_msvc_arm64_cross_ci",
+ "inherits": [
+ "base_ci",
+ "windows_msvc_arm64_cross_base"
+ ],
+ "displayName": "Windows MSVC (ARM64 cross, CI)",
+ "cacheVariables": {
+ "Launcher_BUILD_ARTIFACT": "Windows-MSVC-arm64-Qt6"
+ }
+ }
+ ],
+ "buildPresets": [
+ {
+ "name": "windows_msvc_base",
+ "hidden": true,
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Windows"
+ }
+ },
+ {
+ "name": "windows_msvc_debug",
+ "inherits": [
+ "windows_msvc_base"
+ ],
+ "displayName": "Windows MSVC (Debug)",
+ "configurePreset": "windows_msvc_debug",
+ "configuration": "Debug"
+ },
+ {
+ "name": "windows_msvc_release",
+ "inherits": [
+ "windows_msvc_base"
+ ],
+ "displayName": "Windows MSVC (Release)",
+ "configurePreset": "windows_msvc_release",
+ "configuration": "Release",
+ "nativeToolOptions": [
+ "/p:UseMultiToolTask=true",
+ "/p:EnforceProcessCountAcrossBuilds=true"
+ ]
+ },
+ {
+ "name": "windows_msvc_arm64_cross_debug",
+ "inherits": [
+ "windows_msvc_base"
+ ],
+ "displayName": "Windows MSVC (ARM64 cross, Debug)",
+ "configurePreset": "windows_msvc_arm64_cross_debug",
+ "configuration": "Debug",
+ "nativeToolOptions": [
+ "/p:UseMultiToolTask=true",
+ "/p:EnforceProcessCountAcrossBuilds=true"
+ ]
+ },
+ {
+ "name": "windows_msvc_arm64_cross_release",
+ "inherits": [
+ "windows_msvc_base"
+ ],
+ "displayName": "Windows MSVC (ARM64 cross, Release)",
+ "configurePreset": "windows_msvc_arm64_cross_release",
+ "configuration": "Release",
+ "nativeToolOptions": [
+ "/p:UseMultiToolTask=true",
+ "/p:EnforceProcessCountAcrossBuilds=true"
+ ]
+ },
+ {
+ "name": "windows_msvc_ci",
+ "inherits": [
+ "windows_msvc_base"
+ ],
+ "displayName": "Windows MSVC (CI)",
+ "configurePreset": "windows_msvc_ci",
+ "configuration": "Release",
+ "nativeToolOptions": [
+ "/p:UseMultiToolTask=true",
+ "/p:EnforceProcessCountAcrossBuilds=true"
+ ]
+ },
+ {
+ "name": "windows_msvc_arm64_cross_ci",
+ "inherits": [
+ "windows_msvc_base"
+ ],
+ "displayName": "Windows MSVC (ARM64 cross, CI)",
+ "configurePreset": "windows_msvc_arm64_cross_ci",
+ "configuration": "Release",
+ "nativeToolOptions": [
+ "/p:UseMultiToolTask=true",
+ "/p:EnforceProcessCountAcrossBuilds=true"
+ ]
+ }
+ ],
+ "testPresets": [
+ {
+ "name": "windows_msvc_base",
+ "hidden": true,
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Windows"
+ }
+ },
+ {
+ "name": "windows_msvc_debug",
+ "inherits": [
+ "base_debug",
+ "windows_msvc_base"
+ ],
+ "displayName": "Windows MSVC (Debug)",
+ "configurePreset": "windows_msvc_debug",
+ "configuration": "Debug"
+ },
+ {
+ "name": "windows_msvc_release",
+ "inherits": [
+ "base_release",
+ "windows_msvc_base"
+ ],
+ "displayName": "Windows MSVC (Release)",
+ "configurePreset": "windows_msvc_release",
+ "configuration": "Release"
+ },
+ {
+ "name": "windows_msvc_ci",
+ "inherits": [
+ "base_release",
+ "windows_msvc_base"
+ ],
+ "displayName": "Windows MSVC (CI)",
+ "configurePreset": "windows_msvc_ci",
+ "configuration": "Release"
+ }
+ ],
+ "workflowPresets": [
+ {
+ "name": "windows_msvc_debug",
+ "displayName": "Windows MSVC (Debug)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "windows_msvc_debug"
+ },
+ {
+ "type": "build",
+ "name": "windows_msvc_debug"
+ },
+ {
+ "type": "test",
+ "name": "windows_msvc_debug"
+ }
+ ]
+ },
+ {
+ "name": "windows_msvc",
+ "displayName": "Windows MSVC (Release)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "windows_msvc_release"
+ },
+ {
+ "type": "build",
+ "name": "windows_msvc_release"
+ },
+ {
+ "type": "test",
+ "name": "windows_msvc_release"
+ }
+ ]
+ },
+ {
+ "name": "windows_msvc_arm64_cross_debug",
+ "displayName": "Windows MSVC (ARM64 cross, Debug)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "windows_msvc_arm64_cross_debug"
+ },
+ {
+ "type": "build",
+ "name": "windows_msvc_arm64_cross_debug"
+ }
+ ]
+ },
+ {
+ "name": "windows_msvc_arm64_cross",
+ "displayName": "Windows MSVC (ARM64 cross, Release)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "windows_msvc_arm64_cross_release"
+ },
+ {
+ "type": "build",
+ "name": "windows_msvc_arm64_cross_release"
+ }
+ ]
+ },
+ {
+ "name": "windows_msvc_ci",
+ "displayName": "Windows MSVC (CI)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "windows_msvc_ci"
+ },
+ {
+ "type": "build",
+ "name": "windows_msvc_ci"
+ },
+ {
+ "type": "test",
+ "name": "windows_msvc_ci"
+ }
+ ]
+ },
+ {
+ "name": "windows_msvc_arm64_cross_ci",
+ "displayName": "Windows MSVC (ARM64 cross, CI)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "windows_msvc_arm64_cross_ci"
+ },
+ {
+ "type": "build",
+ "name": "windows_msvc_arm64_cross_ci"
+ }
+ ]
+ }
+ ]
+}
diff --git a/cmake/windowsMinGWPreset.json b/cmake/windowsMinGWPreset.json
new file mode 100644
index 000000000..984caadd6
--- /dev/null
+++ b/cmake/windowsMinGWPreset.json
@@ -0,0 +1,183 @@
+{
+ "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
+ "version": 8,
+ "include": [
+ "commonPresets.json"
+ ],
+ "configurePresets": [
+ {
+ "name": "windows_mingw_base",
+ "hidden": true,
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Windows"
+ },
+ "generator": "Ninja",
+ "cacheVariables": {
+ "Launcher_BUILD_ARTIFACT": "Windows-MinGW-w64-Qt6"
+ }
+ },
+ {
+ "name": "windows_mingw_debug",
+ "inherits": [
+ "base_debug",
+ "windows_mingw_base"
+ ],
+ "displayName": "Windows MinGW (Debug)"
+ },
+ {
+ "name": "windows_mingw_release",
+ "inherits": [
+ "base_release",
+ "windows_mingw_base"
+ ],
+ "displayName": "Windows MinGW (Release)"
+ },
+ {
+ "name": "windows_mingw_ci",
+ "inherits": [
+ "base_ci",
+ "windows_mingw_base"
+ ],
+ "displayName": "Windows MinGW (CI)",
+ "cacheVariables": {
+ "Launcher_BUILD_ARTIFACT": "Windows-MinGW-w64-Qt6"
+ }
+ }
+ ],
+ "buildPresets": [
+ {
+ "name": "windows_mingw_base",
+ "hidden": true,
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Windows"
+ }
+ },
+ {
+ "name": "windows_mingw_debug",
+ "inherits": [
+ "windows_mingw_base"
+ ],
+ "displayName": "Windows MinGW (Debug)",
+ "configurePreset": "windows_mingw_debug"
+ },
+ {
+ "name": "windows_mingw_release",
+ "inherits": [
+ "windows_mingw_base"
+ ],
+ "displayName": "Windows MinGW (Release)",
+ "configurePreset": "windows_mingw_release"
+ },
+ {
+ "name": "windows_mingw_ci",
+ "inherits": [
+ "windows_mingw_base"
+ ],
+ "displayName": "Windows MinGW (CI)",
+ "configurePreset": "windows_mingw_ci"
+ }
+ ],
+ "testPresets": [
+ {
+ "name": "windows_mingw_base",
+ "hidden": true,
+ "condition": {
+ "type": "equals",
+ "lhs": "${hostSystemName}",
+ "rhs": "Windows"
+ },
+ "filter": {
+ "exclude": {
+ "name": "^example64|example$"
+ }
+ }
+ },
+ {
+ "name": "windows_mingw_debug",
+ "inherits": [
+ "base_debug",
+ "windows_mingw_base"
+ ],
+ "displayName": "Windows MinGW (Debug)",
+ "configurePreset": "windows_mingw_debug"
+ },
+ {
+ "name": "windows_mingw_release",
+ "inherits": [
+ "base_release",
+ "windows_mingw_base"
+ ],
+ "displayName": "Windows MinGW (Release)",
+ "configurePreset": "windows_mingw_release"
+ },
+ {
+ "name": "windows_mingw_ci",
+ "inherits": [
+ "base_release",
+ "windows_mingw_base"
+ ],
+ "displayName": "Windows MinGW (CI)",
+ "configurePreset": "windows_mingw_ci"
+ }
+ ],
+ "workflowPresets": [
+ {
+ "name": "windows_mingw_debug",
+ "displayName": "Windows MinGW (Debug)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "windows_mingw_debug"
+ },
+ {
+ "type": "build",
+ "name": "windows_mingw_debug"
+ },
+ {
+ "type": "test",
+ "name": "windows_mingw_debug"
+ }
+ ]
+ },
+ {
+ "name": "windows_mingw",
+ "displayName": "Windows MinGW (Release)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "windows_mingw_release"
+ },
+ {
+ "type": "build",
+ "name": "windows_mingw_release"
+ },
+ {
+ "type": "test",
+ "name": "windows_mingw_release"
+ }
+ ]
+ },
+ {
+ "name": "windows_mingw_ci",
+ "displayName": "Windows MinGW (CI)",
+ "steps": [
+ {
+ "type": "configure",
+ "name": "windows_mingw_ci"
+ },
+ {
+ "type": "build",
+ "name": "windows_mingw_ci"
+ },
+ {
+ "type": "test",
+ "name": "windows_mingw_ci"
+ }
+ ]
+ }
+ ]
+}
diff --git a/flake.lock b/flake.lock
index 07fa5117a..2d2f820f4 100644
--- a/flake.lock
+++ b/flake.lock
@@ -18,11 +18,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1744932701,
- "narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=",
+ "lastModified": 1748460289,
+ "narHash": "sha256-7doLyJBzCllvqX4gszYtmZUToxKvMUrg45EUWaUYmBg=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef",
+ "rev": "96ec055edbe5ee227f28cdbc3f1ddf1df5965102",
"type": "github"
},
"original": {
@@ -32,7 +32,7 @@
"type": "github"
}
},
- "qt-qrcodegenerator": {
+ "qrcodegenerator": {
"flake": false,
"locked": {
"lastModified": 1737616857,
@@ -52,7 +52,7 @@
"inputs": {
"libnbtplusplus": "libnbtplusplus",
"nixpkgs": "nixpkgs",
- "qt-qrcodegenerator": "qt-qrcodegenerator"
+ "qrcodegenerator": "qrcodegenerator"
}
}
},
diff --git a/flake.nix b/flake.nix
index 69abd78dd..751ef2eeb 100644
--- a/flake.nix
+++ b/flake.nix
@@ -16,7 +16,7 @@
flake = false;
};
- qt-qrcodegenerator = {
+ qrcodegenerator = {
url = "github:nayuki/QR-Code-generator";
flake = false;
};
@@ -27,7 +27,7 @@
self,
nixpkgs,
libnbtplusplus,
- qt-qrcodegenerator,
+ qrcodegenerator,
}:
let
@@ -175,7 +175,7 @@
prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix {
inherit
libnbtplusplus
- qt-qrcodegenerator
+ qrcodegenerator
self
;
};
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index 0449a7055..c22e2622b 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -128,6 +128,7 @@
#include
#include
+#include
#include "SysInfo.h"
#ifdef Q_OS_LINUX
@@ -1887,17 +1888,6 @@ QString Application::getUserAgent()
return BuildConfig.USER_AGENT;
}
-QString Application::getUserAgentUncached()
-{
- QString uaOverride = m_settings->get("UserAgentOverride").toString();
- if (!uaOverride.isEmpty()) {
- uaOverride += " (Uncached)";
- return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString());
- }
-
- return BuildConfig.USER_AGENT_UNCACHED;
-}
-
bool Application::handleDataMigration(const QString& currentData,
const QString& oldData,
const QString& name,
diff --git a/launcher/Application.h b/launcher/Application.h
index 12f41509c..fefb32292 100644
--- a/launcher/Application.h
+++ b/launcher/Application.h
@@ -160,7 +160,6 @@ class Application : public QApplication {
QString getFlameAPIKey();
QString getModrinthAPIToken();
QString getUserAgent();
- QString getUserAgentUncached();
/// this is the root of the 'installation'. Used for automatic updates
const QString& root() { return m_rootPath; }
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 847d3ff0e..928c15325 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -310,6 +310,8 @@ set(MINECRAFT_SOURCES
minecraft/ParseUtils.h
minecraft/ProfileUtils.cpp
minecraft/ProfileUtils.h
+ minecraft/ShortcutUtils.cpp
+ minecraft/ShortcutUtils.h
minecraft/Library.cpp
minecraft/Library.h
minecraft/MojangDownloadInfo.h
@@ -834,6 +836,10 @@ SET(LAUNCHER_SOURCES
icons/IconList.h
icons/IconList.cpp
+ # log utils
+ logs/AnonymizeLog.cpp
+ logs/AnonymizeLog.h
+
# GUI - windows
ui/GuiUtil.h
ui/GuiUtil.cpp
@@ -1048,6 +1054,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ProfileSetupDialog.h
ui/dialogs/CopyInstanceDialog.cpp
ui/dialogs/CopyInstanceDialog.h
+ ui/dialogs/CreateShortcutDialog.cpp
+ ui/dialogs/CreateShortcutDialog.h
ui/dialogs/CustomMessageBox.cpp
ui/dialogs/CustomMessageBox.h
ui/dialogs/ExportInstanceDialog.cpp
@@ -1230,6 +1238,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/widgets/MinecraftSettingsWidget.ui
ui/widgets/JavaSettingsWidget.ui
ui/dialogs/CopyInstanceDialog.ui
+ ui/dialogs/CreateShortcutDialog.ui
ui/dialogs/ProfileSetupDialog.ui
ui/dialogs/ProgressDialog.ui
ui/dialogs/NewInstanceDialog.ui
@@ -1306,7 +1315,7 @@ target_link_libraries(Launcher_logic
qdcss
BuildConfig
Qt${QT_VERSION_MAJOR}::Widgets
- qrcode
+ qrcodegenerator
)
if (UNIX AND NOT CYGWIN AND NOT APPLE)
diff --git a/launcher/FileIgnoreProxy.cpp b/launcher/FileIgnoreProxy.cpp
index 0314057d1..cebe82eda 100644
--- a/launcher/FileIgnoreProxy.cpp
+++ b/launcher/FileIgnoreProxy.cpp
@@ -269,9 +269,9 @@ bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const
return m_ignoreFiles.contains(fileInfo.fileName()) || m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath()));
}
-bool FileIgnoreProxy::filterFile(const QString& fileName) const
+bool FileIgnoreProxy::filterFile(const QFileInfo& file) const
{
- return m_blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(m_root), fileName));
+ return m_blocked.covers(relPath(file.absoluteFilePath())) || ignoreFile(file);
}
void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName)
diff --git a/launcher/FileIgnoreProxy.h b/launcher/FileIgnoreProxy.h
index 25d85ab60..5184fc354 100644
--- a/launcher/FileIgnoreProxy.h
+++ b/launcher/FileIgnoreProxy.h
@@ -69,7 +69,7 @@ class FileIgnoreProxy : public QSortFilterProxyModel {
// list of relative paths that need to be removed completely from model
inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; }
- bool filterFile(const QString& fileName) const;
+ bool filterFile(const QFileInfo& fileName) const;
void loadBlockedPathsFromFile(const QString& fileName);
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
index 08dc7d2cc..9d45f4af3 100644
--- a/launcher/FileSystem.cpp
+++ b/launcher/FileSystem.cpp
@@ -107,6 +107,10 @@ namespace fs = std::filesystem;
#if defined(__MINGW32__)
+// Avoid re-defining structs retroactively added to MinGW
+// https://github.com/mingw-w64/mingw-w64/issues/90#issuecomment-2829284729
+#if __MINGW64_VERSION_MAJOR < 13
+
struct _DUPLICATE_EXTENTS_DATA {
HANDLE FileHandle;
LARGE_INTEGER SourceFileOffset;
@@ -116,6 +120,7 @@ struct _DUPLICATE_EXTENTS_DATA {
using DUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA;
using PDUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA*;
+#endif
struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER {
WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32
@@ -887,6 +892,11 @@ QString getDesktopDir()
return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
}
+QString getApplicationsDir()
+{
+ return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
+}
+
// Cross-platform Shortcut creation
bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon)
{
@@ -898,16 +908,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
return false;
}
#if defined(Q_OS_MACOS)
- // Create the Application
- QDir applicationDirectory =
- QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + "/" + BuildConfig.LAUNCHER_NAME + " Instances/";
-
- if (!applicationDirectory.mkpath(".")) {
- qWarning() << "Couldn't create application directory";
- return false;
- }
-
- QDir application = applicationDirectory.path() + "/" + name + ".app/";
+ QDir application = destination + ".app/";
if (application.exists()) {
qWarning() << "Application already exists!";
diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h
index bf91c603c..b1108eded 100644
--- a/launcher/FileSystem.h
+++ b/launcher/FileSystem.h
@@ -353,6 +353,9 @@ bool checkProblemticPathJava(QDir folder);
// Get the Directory representing the User's Desktop
QString getDesktopDir();
+// Get the Directory representing the User's Applications directory
+QString getApplicationsDir();
+
// Overrides one folder with the contents of another, preserving items exclusive to the first folder
// Equivalent to doing QDir::rename, but allowing for overrides
bool overrideFolder(QString overwritten_path, QString override_path);
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
index 633382404..c489f6112 100644
--- a/launcher/InstanceImportTask.cpp
+++ b/launcher/InstanceImportTask.cpp
@@ -72,7 +72,6 @@ bool InstanceImportTask::abort()
bool wasAborted = false;
if (m_task)
wasAborted = m_task->abort();
- Task::abort();
return wasAborted;
}
@@ -212,6 +211,7 @@ void InstanceImportTask::processZipPack()
progressStep->status = status;
stepProgress(*progressStep);
});
+ connect(zipTask.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); });
m_task.reset(zipTask);
zipTask->start();
}
@@ -263,6 +263,25 @@ void InstanceImportTask::extractFinished()
}
}
+bool installIcon(QString root, QString instIcon)
+{
+ auto importIconPath = IconUtils::findBestIconIn(root, instIcon);
+ if (importIconPath.isNull() || !QFile::exists(importIconPath))
+ importIconPath = IconUtils::findBestIconIn(root, "icon.png");
+ if (importIconPath.isNull() || !QFile::exists(importIconPath))
+ importIconPath = IconUtils::findBestIconIn(FS::PathCombine(root, "overrides"), "icon.png");
+ if (!importIconPath.isNull() && QFile::exists(importIconPath)) {
+ // import icon
+ auto iconList = APPLICATION->icons();
+ if (iconList->iconFileExists(instIcon)) {
+ iconList->deleteIcon(instIcon);
+ }
+ iconList->installIcon(importIconPath, instIcon);
+ return true;
+ }
+ return false;
+}
+
void InstanceImportTask::processFlame()
{
shared_qobject_ptr inst_creation_task = nullptr;
@@ -288,6 +307,14 @@ void InstanceImportTask::processFlame()
}
inst_creation_task->setName(*this);
+ // if the icon was specified by user, use that. otherwise pull icon from the pack
+ if (m_instIcon == "default") {
+ auto iconKey = QString("Flame_%1_Icon").arg(name());
+
+ if (installIcon(m_stagingPath, iconKey)) {
+ m_instIcon = iconKey;
+ }
+ }
inst_creation_task->setIcon(m_instIcon);
inst_creation_task->setGroup(m_instGroup);
inst_creation_task->setConfirmUpdate(shouldConfirmUpdate());
@@ -305,9 +332,11 @@ void InstanceImportTask::processFlame()
connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails);
- connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort);
+ connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted);
connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable);
+ connect(inst_creation_task.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); });
+
m_task.reset(inst_creation_task);
setAbortable(true);
m_task->start();
@@ -340,17 +369,7 @@ void InstanceImportTask::processMultiMC()
} else {
m_instIcon = instance.iconKey();
- auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon);
- if (importIconPath.isNull() || !QFile::exists(importIconPath))
- importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), "icon.png");
- if (!importIconPath.isNull() && QFile::exists(importIconPath)) {
- // import icon
- auto iconList = APPLICATION->icons();
- if (iconList->iconFileExists(m_instIcon)) {
- iconList->deleteIcon(m_instIcon);
- }
- iconList->installIcon(importIconPath, m_instIcon);
- }
+ installIcon(instance.instanceRoot(), m_instIcon);
}
emitSucceeded();
}
@@ -387,6 +406,14 @@ void InstanceImportTask::processModrinth()
}
inst_creation_task->setName(*this);
+ // if the icon was specified by user, use that. otherwise pull icon from the pack
+ if (m_instIcon == "default") {
+ auto iconKey = QString("Modrinth_%1_Icon").arg(name());
+
+ if (installIcon(m_stagingPath, iconKey)) {
+ m_instIcon = iconKey;
+ }
+ }
inst_creation_task->setIcon(m_instIcon);
inst_creation_task->setGroup(m_instGroup);
inst_creation_task->setConfirmUpdate(shouldConfirmUpdate());
@@ -404,9 +431,11 @@ void InstanceImportTask::processModrinth()
connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails);
- connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort);
+ connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted);
connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable);
+ connect(inst_creation_task.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); });
+
m_task.reset(inst_creation_task);
setAbortable(true);
m_task->start();
diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp
index b38aca17a..0b1a2b39e 100644
--- a/launcher/MMCZip.cpp
+++ b/launcher/MMCZip.cpp
@@ -418,7 +418,7 @@ bool extractFile(QString fileCompressed, QString file, QString target)
return extractRelFile(&zip, file, target);
}
-bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter)
+bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter)
{
QDir rootDirectory(rootDir);
if (!rootDirectory.exists())
@@ -443,8 +443,8 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q
// collect files
entries = directory.entryInfoList(QDir::Files);
for (const auto& e : entries) {
- QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath());
- if (excludeFilter && excludeFilter(relativeFilePath)) {
+ if (excludeFilter && excludeFilter(e)) {
+ QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath());
qDebug() << "Skipping file " << relativeFilePath;
continue;
}
diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h
index d81df9d81..fe0c79de2 100644
--- a/launcher/MMCZip.h
+++ b/launcher/MMCZip.h
@@ -56,6 +56,7 @@
namespace MMCZip {
using FilterFunction = std::function;
+using FilterFileFunction = std::function;
/**
* Merge two zip files, using a filter function
@@ -149,7 +150,7 @@ bool extractFile(QString fileCompressed, QString file, QString dir);
* \param excludeFilter function to excludeFilter which files shouldn't be included (returning true means to excude)
* \return true for success or false for failure
*/
-bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter);
+bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter);
#if defined(LAUNCHER_APPLICATION)
class ExportToZipTask : public Task {
diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp
index 072cb1d16..2d0560049 100644
--- a/launcher/java/JavaUtils.cpp
+++ b/launcher/java/JavaUtils.cpp
@@ -365,13 +365,13 @@ QList JavaUtils::FindJavaPaths()
javas.append("/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java");
QDir libraryJVMDir("/Library/Java/JavaVirtualMachines/");
QStringList libraryJVMJavas = libraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
- foreach (const QString& java, libraryJVMJavas) {
+ for (const QString& java : libraryJVMJavas) {
javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/jre/bin/java");
}
QDir systemLibraryJVMDir("/System/Library/Java/JavaVirtualMachines/");
QStringList systemLibraryJVMJavas = systemLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
- foreach (const QString& java, systemLibraryJVMJavas) {
+ for (const QString& java : systemLibraryJVMJavas) {
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java");
}
@@ -381,14 +381,14 @@ QList JavaUtils::FindJavaPaths()
// javas downloaded by sdkman
QDir sdkmanDir(FS::PathCombine(home, ".sdkman/candidates/java"));
QStringList sdkmanJavas = sdkmanDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
- foreach (const QString& java, sdkmanJavas) {
+ for (const QString& java : sdkmanJavas) {
javas.append(sdkmanDir.absolutePath() + "/" + java + "/bin/java");
}
// java in user library folder (like from intellij downloads)
QDir userLibraryJVMDir(FS::PathCombine(home, "Library/Java/JavaVirtualMachines/"));
QStringList userLibraryJVMJavas = userLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
- foreach (const QString& java, userLibraryJVMJavas) {
+ for (const QString& java : userLibraryJVMJavas) {
javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java");
}
diff --git a/launcher/java/download/ArchiveDownloadTask.cpp b/launcher/java/download/ArchiveDownloadTask.cpp
index bb7cc568d..bb31ca1e2 100644
--- a/launcher/java/download/ArchiveDownloadTask.cpp
+++ b/launcher/java/download/ArchiveDownloadTask.cpp
@@ -55,6 +55,7 @@ void ArchiveDownloadTask::executeTask()
connect(download.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress);
connect(download.get(), &Task::status, this, &ArchiveDownloadTask::setStatus);
connect(download.get(), &Task::details, this, &ArchiveDownloadTask::setDetails);
+ connect(download.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted);
connect(download.get(), &Task::succeeded, [this, fullPath] {
// This should do all of the extracting and creating folders
extractJava(fullPath);
@@ -135,7 +136,6 @@ bool ArchiveDownloadTask::abort()
auto aborted = canAbort();
if (m_task)
aborted = m_task->abort();
- emitAborted();
return aborted;
};
} // namespace Java
\ No newline at end of file
diff --git a/launcher/launch/LogModel.cpp b/launcher/launch/LogModel.cpp
index 53a450ff7..90af9787d 100644
--- a/launcher/launch/LogModel.cpp
+++ b/launcher/launch/LogModel.cpp
@@ -167,8 +167,8 @@ bool LogModel::isOverFlow()
return m_numLines >= m_maxLines && m_stopOnOverflow;
}
-
-MessageLevel::Enum LogModel::previousLevel() {
+MessageLevel::Enum LogModel::previousLevel()
+{
if (!m_content.isEmpty()) {
return m_content.last().level;
}
diff --git a/launcher/logs/AnonymizeLog.cpp b/launcher/logs/AnonymizeLog.cpp
new file mode 100644
index 000000000..e5021a616
--- /dev/null
+++ b/launcher/logs/AnonymizeLog.cpp
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2025 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 "AnonymizeLog.h"
+
+#include
+
+struct RegReplace {
+ RegReplace(QRegularExpression r, QString w) : reg(r), with(w) { reg.optimize(); }
+ QRegularExpression reg;
+ QString with;
+};
+
+static const QVector anonymizeRules = {
+ RegReplace(QRegularExpression("C:\\\\Users\\\\([^\\\\]+)\\\\", QRegularExpression::CaseInsensitiveOption),
+ "C:\\Users\\********\\"), // windows
+ RegReplace(QRegularExpression("C:\\/Users\\/([^\\/]+)\\/", QRegularExpression::CaseInsensitiveOption),
+ "C:/Users/********/"), // windows with forward slashes
+ RegReplace(QRegularExpression("(?)"), // SESSION_TOKEN
+ RegReplace(QRegularExpression("new refresh token: \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption),
+ "new refresh token: \"\""), // refresh token
+ RegReplace(QRegularExpression("\"device_code\" : \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption),
+ "\"device_code\" : \"\""), // device code
+};
+
+void anonymizeLog(QString& log)
+{
+ for (auto rule : anonymizeRules) {
+ log.replace(rule.reg, rule.with);
+ }
+}
\ No newline at end of file
diff --git a/launcher/logs/AnonymizeLog.h b/launcher/logs/AnonymizeLog.h
new file mode 100644
index 000000000..2409ecee7
--- /dev/null
+++ b/launcher/logs/AnonymizeLog.h
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2025 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
+
+void anonymizeLog(QString& log);
\ No newline at end of file
diff --git a/launcher/logs/LogParser.cpp b/launcher/logs/LogParser.cpp
index 6e33b24dd..0790dec4d 100644
--- a/launcher/logs/LogParser.cpp
+++ b/launcher/logs/LogParser.cpp
@@ -107,7 +107,7 @@ std::optional LogParser::parseNext()
if (m_buffer.trimmed().isEmpty()) {
auto text = QString(m_buffer);
m_buffer.clear();
- return LogParser::PlainText { text };
+ return LogParser::PlainText{ text };
}
// check if we have a full xml log4j event
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index 4d37aad9c..77e2294a6 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -248,6 +248,7 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerSetting("ExportSummary", "");
m_settings->registerSetting("ExportAuthor", "");
m_settings->registerSetting("ExportOptionalFiles", true);
+ m_settings->registerSetting("ExportRecommendedRAM");
auto dataPacksEnabled = m_settings->registerSetting("GlobalDataPacksEnabled", false);
auto dataPacksPath = m_settings->registerSetting("GlobalDataPacksPath", "");
@@ -1019,7 +1020,6 @@ QMap MinecraftInstance::createCensorFilterFromSession(AuthSess
return filter;
}
-
QStringList MinecraftInstance::getLogFileSearchPaths()
{
return { FS::PathCombine(gameRoot(), "crash-reports"), FS::PathCombine(gameRoot(), "logs"), gameRoot() };
diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp
new file mode 100644
index 000000000..43954aa6a
--- /dev/null
+++ b/launcher/minecraft/ShortcutUtils.cpp
@@ -0,0 +1,237 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu
+ * Copyright (C) 2023 TheKodeToad
+ * Copyright (C) 2025 Yihe Li
+ *
+ * parent 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.
+ *
+ * parent 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 parent program. If not, see .
+ *
+ * parent 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 parent 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 "ShortcutUtils.h"
+
+#include "FileSystem.h"
+
+#include
+#include
+
+#include
+#include
+#include
+
+namespace ShortcutUtils {
+
+void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath)
+{
+ if (!shortcut.instance)
+ return;
+
+ QString appPath = QApplication::applicationFilePath();
+ auto icon = APPLICATION->icons()->icon(shortcut.iconKey.isEmpty() ? shortcut.instance->iconKey() : shortcut.iconKey);
+ if (icon == nullptr) {
+ icon = APPLICATION->icons()->icon("grass");
+ }
+ QString iconPath;
+ QStringList args;
+#if defined(Q_OS_MACOS)
+ if (appPath.startsWith("/private/var/")) {
+ QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"),
+ QObject::tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts."));
+ return;
+ }
+
+ iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "Icon.icns");
+
+ QFile iconFile(iconPath);
+ if (!iconFile.open(QFile::WriteOnly)) {
+ QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application."));
+ return;
+ }
+
+ QIcon iconObj = icon->icon();
+ bool success = iconObj.pixmap(1024, 1024).save(iconPath, "ICNS");
+ iconFile.close();
+
+ if (!success) {
+ iconFile.remove();
+ QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application."));
+ return;
+ }
+#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+ if (appPath.startsWith("/tmp/.mount_")) {
+ // AppImage!
+ appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE"));
+ if (appPath.isEmpty()) {
+ QMessageBox::critical(
+ shortcut.parent, QObject::tr("Create Shortcut"),
+ QObject::tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)"));
+ } else if (appPath.endsWith("/")) {
+ appPath.chop(1);
+ }
+ }
+
+ iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.png");
+
+ QFile iconFile(iconPath);
+ if (!iconFile.open(QFile::WriteOnly)) {
+ QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut."));
+ return;
+ }
+ bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG");
+ iconFile.close();
+
+ if (!success) {
+ iconFile.remove();
+ QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut."));
+ return;
+ }
+
+ if (DesktopServices::isFlatpak()) {
+ appPath = "flatpak";
+ args.append({ "run", BuildConfig.LAUNCHER_APPID });
+ }
+
+#elif defined(Q_OS_WIN)
+ iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.ico");
+
+ // part of fix for weird bug involving the window icon being replaced
+ // dunno why it happens, but parent 2-line fix seems to be enough, so w/e
+ auto appIcon = APPLICATION->getThemedIcon("logo");
+
+ QFile iconFile(iconPath);
+ if (!iconFile.open(QFile::WriteOnly)) {
+ QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut."));
+ return;
+ }
+ bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO");
+ iconFile.close();
+
+ // restore original window icon
+ QGuiApplication::setWindowIcon(appIcon);
+
+ if (!success) {
+ iconFile.remove();
+ QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut."));
+ return;
+ }
+
+#else
+ QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Not supported on your platform!"));
+ return;
+#endif
+ args.append({ "--launch", shortcut.instance->id() });
+ args.append(shortcut.extraArgs);
+
+ if (!FS::createShortcut(filePath, appPath, args, shortcut.name, iconPath)) {
+#if not defined(Q_OS_MACOS)
+ iconFile.remove();
+#endif
+ QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"),
+ QObject::tr("Failed to create %1 shortcut!").arg(shortcut.targetString));
+ }
+}
+
+void createInstanceShortcutOnDesktop(const Shortcut& shortcut)
+{
+ if (!shortcut.instance)
+ return;
+
+ QString desktopDir = FS::getDesktopDir();
+ if (desktopDir.isEmpty()) {
+ QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find desktop?!"));
+ return;
+ }
+
+ QString shortcutFilePath = FS::PathCombine(desktopDir, FS::RemoveInvalidFilenameChars(shortcut.name));
+ createInstanceShortcut(shortcut, shortcutFilePath);
+ QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"),
+ QObject::tr("Created a shortcut to this %1 on your desktop!").arg(shortcut.targetString));
+}
+
+void createInstanceShortcutInApplications(const Shortcut& shortcut)
+{
+ if (!shortcut.instance)
+ return;
+
+ QString applicationsDir = FS::getApplicationsDir();
+ if (applicationsDir.isEmpty()) {
+ QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find applications folder?!"));
+ return;
+ }
+
+#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
+ applicationsDir = FS::PathCombine(applicationsDir, BuildConfig.LAUNCHER_DISPLAYNAME + " Instances");
+
+ QDir applicationsDirQ(applicationsDir);
+ if (!applicationsDirQ.mkpath(".")) {
+ QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"),
+ QObject::tr("Failed to create instances folder in applications folder!"));
+ return;
+ }
+#endif
+
+ QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(shortcut.name));
+ createInstanceShortcut(shortcut, shortcutFilePath);
+ QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"),
+ QObject::tr("Created a shortcut to this %1 in your applications folder!").arg(shortcut.targetString));
+}
+
+void createInstanceShortcutInOther(const Shortcut& shortcut)
+{
+ if (!shortcut.instance)
+ return;
+
+ QString defaultedDir = FS::getDesktopDir();
+#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+ QString extension = ".desktop";
+#elif defined(Q_OS_WINDOWS)
+ QString extension = ".lnk";
+#else
+ QString extension = "";
+#endif
+
+ QString shortcutFilePath = FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(shortcut.name) + extension);
+ QFileDialog fileDialog;
+ // workaround to make sure the portal file dialog opens in the desktop directory
+ fileDialog.setDirectoryUrl(defaultedDir);
+
+ shortcutFilePath = fileDialog.getSaveFileName(shortcut.parent, QObject::tr("Create Shortcut"), shortcutFilePath,
+ QObject::tr("Desktop Entries") + " (*" + extension + ")");
+ if (shortcutFilePath.isEmpty())
+ return; // file dialog canceled by user
+
+ if (shortcutFilePath.endsWith(extension))
+ shortcutFilePath = shortcutFilePath.mid(0, shortcutFilePath.length() - extension.length());
+ createInstanceShortcut(shortcut, shortcutFilePath);
+ QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"),
+ QObject::tr("Created a shortcut to this %1!").arg(shortcut.targetString));
+}
+
+} // namespace ShortcutUtils
diff --git a/launcher/minecraft/ShortcutUtils.h b/launcher/minecraft/ShortcutUtils.h
new file mode 100644
index 000000000..e3d2e283a
--- /dev/null
+++ b/launcher/minecraft/ShortcutUtils.h
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu
+ * Copyright (C) 2023 TheKodeToad
+ * Copyright (C) 2025 Yihe Li
+ *
+ * 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 "Application.h"
+
+#include
+
+namespace ShortcutUtils {
+/// A struct to hold parameters for creating a shortcut
+struct Shortcut {
+ BaseInstance* instance;
+ QString name;
+ QString targetString;
+ QWidget* parent = nullptr;
+ QStringList extraArgs = {};
+ QString iconKey = "";
+};
+
+/// Create an instance shortcut on the specified file path
+void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath);
+
+/// Create an instance shortcut on the desktop
+void createInstanceShortcutOnDesktop(const Shortcut& shortcut);
+
+/// Create an instance shortcut in the Applications directory
+void createInstanceShortcutInApplications(const Shortcut& shortcut);
+
+/// Create an instance shortcut in other directories
+void createInstanceShortcutInOther(const Shortcut& shortcut);
+
+} // namespace ShortcutUtils
diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h
index deb1859de..553af92f3 100644
--- a/launcher/minecraft/mod/Mod.h
+++ b/launcher/minecraft/mod/Mod.h
@@ -84,7 +84,7 @@ class Mod : public Resource {
bool valid() const override;
- [[nodiscard]] int compare(const Resource & other, SortType type) const override;
+ [[nodiscard]] int compare(const Resource& other, SortType type) const override;
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
// Delete all the files of this mod
diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp
index 073ea7ca7..4d7c71359 100644
--- a/launcher/minecraft/mod/TexturePackFolderModel.cpp
+++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp
@@ -48,7 +48,8 @@ TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, BaseInstance* in
m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE };
- m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
+ m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch,
+ QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true, true };
m_columnsHiddenByDefault = { false, false, false, false, false, true };
}
diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp
index 56379aaab..be3bc776b 100644
--- a/launcher/minecraft/skins/SkinList.cpp
+++ b/launcher/minecraft/skins/SkinList.cpp
@@ -268,6 +268,26 @@ void SkinList::installSkins(const QStringList& iconFiles)
installSkin(file);
}
+QString getUniqueFile(const QString& root, const QString& file)
+{
+ auto result = FS::PathCombine(root, file);
+ if (!QFileInfo::exists(result)) {
+ return result;
+ }
+
+ QString baseName = QFileInfo(file).completeBaseName();
+ QString extension = QFileInfo(file).suffix();
+ int tries = 0;
+ while (QFileInfo::exists(result)) {
+ if (++tries > 256)
+ return {};
+
+ QString key = QString("%1%2.%3").arg(baseName).arg(tries).arg(extension);
+ result = FS::PathCombine(root, key);
+ }
+
+ return result;
+}
QString SkinList::installSkin(const QString& file, const QString& name)
{
if (file.isEmpty())
@@ -282,7 +302,7 @@ QString SkinList::installSkin(const QString& file, const QString& name)
if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid())
return tr("Skin images must be 64x64 or 64x32 pixel PNG files.");
- QString target = FS::PathCombine(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name);
+ QString target = getUniqueFile(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name);
return QFile::copy(file, target) ? "" : tr("Unable to copy file");
}
@@ -371,7 +391,8 @@ bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role)
auto& skin = m_skinList[row];
auto newName = value.toString();
if (skin.name() != newName) {
- skin.rename(newName);
+ if (!skin.rename(newName))
+ return false;
save();
}
return true;
diff --git a/launcher/minecraft/skins/SkinModel.cpp b/launcher/minecraft/skins/SkinModel.cpp
index b609bc6c7..209207215 100644
--- a/launcher/minecraft/skins/SkinModel.cpp
+++ b/launcher/minecraft/skins/SkinModel.cpp
@@ -122,7 +122,11 @@ QString SkinModel::name() const
bool SkinModel::rename(QString newName)
{
auto info = QFileInfo(m_path);
- m_path = FS::PathCombine(info.absolutePath(), newName + ".png");
+ auto new_path = FS::PathCombine(info.absolutePath(), newName + ".png");
+ if (QFileInfo::exists(new_path)) {
+ return false;
+ }
+ m_path = new_path;
return FS::move(info.absoluteFilePath(), m_path);
}
diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
index c30ba5249..5d9c74ccf 100644
--- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
+++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
@@ -54,6 +54,7 @@
#include "settings/INISettingsObject.h"
+#include "sys.h"
#include "tasks/ConcurrentTask.h"
#include "ui/dialogs/BlockedModsDialog.h"
#include "ui/dialogs/CustomMessageBox.h"
@@ -418,6 +419,24 @@ bool FlameCreationTask::createInstance()
}
}
+ int recommendedRAM = m_pack.minecraft.recommendedRAM;
+
+ // only set memory if this is a fresh instance
+ if (m_instance == nullptr && recommendedRAM > 0) {
+ const uint64_t sysMiB = Sys::getSystemRam() / Sys::mebibyte;
+ const uint64_t max = sysMiB * 0.9;
+
+ if (recommendedRAM > max) {
+ logWarning(tr("The recommended memory of the modpack exceeds 90% of your system RAM—reducing it from %1 MiB to %2 MiB!")
+ .arg(recommendedRAM)
+ .arg(max));
+ recommendedRAM = max;
+ }
+
+ instance.settings()->set("OverrideMemory", true);
+ instance.settings()->set("MaxMemAlloc", recommendedRAM);
+ }
+
QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods");
QFileInfo jarmodsInfo(jarmodsPath);
if (jarmodsInfo.isDir()) {
diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp
index 3405b702f..900fd1a87 100644
--- a/launcher/modplatform/flame/FlamePackExportTask.cpp
+++ b/launcher/modplatform/flame/FlamePackExportTask.cpp
@@ -41,22 +41,8 @@
const QString FlamePackExportTask::TEMPLATE = "{name}{authors}\n";
const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" });
-FlamePackExportTask::FlamePackExportTask(const QString& name,
- const QString& version,
- const QString& author,
- bool optionalFiles,
- InstancePtr instance,
- const QString& output,
- MMCZip::FilterFunction filter)
- : name(name)
- , version(version)
- , author(author)
- , optionalFiles(optionalFiles)
- , instance(instance)
- , mcInstance(dynamic_cast(instance.get()))
- , gameRoot(instance->gameRoot())
- , output(output)
- , filter(filter)
+FlamePackExportTask::FlamePackExportTask(FlamePackExportOptions&& options)
+ : m_options(std::move(options)), m_gameRoot(m_options.instance->gameRoot())
{}
void FlamePackExportTask::executeTask()
@@ -70,7 +56,6 @@ bool FlamePackExportTask::abort()
{
if (task) {
task->abort();
- emitAborted();
return true;
}
return false;
@@ -82,7 +67,7 @@ void FlamePackExportTask::collectFiles()
QCoreApplication::processEvents();
files.clear();
- if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) {
+ if (!MMCZip::collectFileListRecursively(m_options.instance->gameRoot(), nullptr, &files, m_options.filter)) {
emitFailed(tr("Could not search for files"));
return;
}
@@ -90,11 +75,8 @@ void FlamePackExportTask::collectFiles()
pendingHashes.clear();
resolvedFiles.clear();
- if (mcInstance != nullptr) {
- mcInstance->loaderModList()->update();
- connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes);
- } else
- collectHashes();
+ m_options.instance->loaderModList()->update();
+ connect(m_options.instance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes);
}
void FlamePackExportTask::collectHashes()
@@ -102,11 +84,11 @@ void FlamePackExportTask::collectHashes()
setAbortable(true);
setStatus(tr("Finding file hashes..."));
setProgress(1, 5);
- auto allMods = mcInstance->loaderModList()->allMods();
+ auto allMods = m_options.instance->loaderModList()->allMods();
ConcurrentTask::Ptr hashingTask(new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()));
task.reset(hashingTask);
for (const QFileInfo& file : files) {
- const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
+ const QString relative = m_gameRoot.relativeFilePath(file.absoluteFilePath());
// require sensible file types
if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) {
return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled");
@@ -171,6 +153,7 @@ void FlamePackExportTask::collectHashes()
progressStep->status = status;
stepProgress(*progressStep);
});
+ connect(hashingTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted);
hashingTask->start();
}
@@ -246,6 +229,7 @@ void FlamePackExportTask::makeApiRequest()
getProjectsInfo();
});
connect(task.get(), &Task::failed, this, &FlamePackExportTask::getProjectsInfo);
+ connect(task.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted);
task->start();
}
@@ -324,6 +308,7 @@ void FlamePackExportTask::getProjectsInfo()
buildZip();
});
connect(projTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
+ connect(projTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted);
task.reset(projTask);
task->start();
}
@@ -333,13 +318,13 @@ void FlamePackExportTask::buildZip()
setStatus(tr("Adding files..."));
setProgress(4, 5);
- auto zipTask = makeShared(output, gameRoot, files, "overrides/", true, false);
+ auto zipTask = makeShared(m_options.output, m_gameRoot, files, "overrides/", true, false);
zipTask->addExtraFile("manifest.json", generateIndex());
zipTask->addExtraFile("modlist.html", generateHTML());
QStringList exclude;
std::transform(resolvedFiles.keyBegin(), resolvedFiles.keyEnd(), std::back_insert_iterator(exclude),
- [this](QString file) { return gameRoot.relativeFilePath(file); });
+ [this](QString file) { return m_gameRoot.relativeFilePath(file); });
zipTask->setExcludeFiles(exclude);
auto progressStep = std::make_shared();
@@ -374,52 +359,56 @@ QByteArray FlamePackExportTask::generateIndex()
QJsonObject obj;
obj["manifestType"] = "minecraftModpack";
obj["manifestVersion"] = 1;
- obj["name"] = name;
- obj["version"] = version;
- obj["author"] = author;
+ obj["name"] = m_options.name;
+ obj["version"] = m_options.version;
+ obj["author"] = m_options.author;
obj["overrides"] = "overrides";
- if (mcInstance) {
- QJsonObject version;
- auto profile = mcInstance->getPackProfile();
- // collect all supported components
- const ComponentPtr minecraft = profile->getComponent("net.minecraft");
- const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
- const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
- const ComponentPtr forge = profile->getComponent("net.minecraftforge");
- const ComponentPtr neoforge = profile->getComponent("net.neoforged");
- // convert all available components to mrpack dependencies
- if (minecraft != nullptr)
- version["version"] = minecraft->m_version;
- QString id;
- if (quilt != nullptr)
- id = "quilt-" + quilt->m_version;
- else if (fabric != nullptr)
- id = "fabric-" + fabric->m_version;
- else if (forge != nullptr)
- id = "forge-" + forge->m_version;
- else if (neoforge != nullptr) {
- id = "neoforge-";
- if (minecraft->m_version == "1.20.1")
- id += "1.20.1-";
- id += neoforge->m_version;
- }
- version["modLoaders"] = QJsonArray();
- if (!id.isEmpty()) {
- QJsonObject loader;
- loader["id"] = id;
- loader["primary"] = true;
- version["modLoaders"] = QJsonArray({ loader });
- }
- obj["minecraft"] = version;
+ QJsonObject version;
+
+ auto profile = m_options.instance->getPackProfile();
+ // collect all supported components
+ const ComponentPtr minecraft = profile->getComponent("net.minecraft");
+ const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
+ const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
+ const ComponentPtr forge = profile->getComponent("net.minecraftforge");
+ const ComponentPtr neoforge = profile->getComponent("net.neoforged");
+
+ // convert all available components to mrpack dependencies
+ if (minecraft != nullptr)
+ version["version"] = minecraft->m_version;
+ QString id;
+ if (quilt != nullptr)
+ id = "quilt-" + quilt->m_version;
+ else if (fabric != nullptr)
+ id = "fabric-" + fabric->m_version;
+ else if (forge != nullptr)
+ id = "forge-" + forge->m_version;
+ else if (neoforge != nullptr) {
+ id = "neoforge-";
+ if (minecraft->m_version == "1.20.1")
+ id += "1.20.1-";
+ id += neoforge->m_version;
}
+ version["modLoaders"] = QJsonArray();
+ if (!id.isEmpty()) {
+ QJsonObject loader;
+ loader["id"] = id;
+ loader["primary"] = true;
+ version["modLoaders"] = QJsonArray({ loader });
+ }
+
+ if (m_options.recommendedRAM > 0)
+ version["recommendedRam"] = m_options.recommendedRAM;
+
+ obj["minecraft"] = version;
QJsonArray files;
for (auto mod : resolvedFiles) {
QJsonObject file;
file["projectID"] = mod.addonId;
file["fileID"] = mod.version;
- file["required"] = mod.enabled || !optionalFiles;
+ file["required"] = mod.enabled || !m_options.optionalFiles;
files << file;
}
obj["files"] = files;
diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h
index b11eb17fa..e3d4c74a7 100644
--- a/launcher/modplatform/flame/FlamePackExportTask.h
+++ b/launcher/modplatform/flame/FlamePackExportTask.h
@@ -19,22 +19,26 @@
#pragma once
-#include "BaseInstance.h"
#include "MMCZip.h"
#include "minecraft/MinecraftInstance.h"
#include "modplatform/flame/FlameAPI.h"
#include "tasks/Task.h"
+struct FlamePackExportOptions {
+ QString name;
+ QString version;
+ QString author;
+ bool optionalFiles;
+ MinecraftInstancePtr instance;
+ QString output;
+ MMCZip::FilterFileFunction filter;
+ int recommendedRAM;
+};
+
class FlamePackExportTask : public Task {
Q_OBJECT
public:
- FlamePackExportTask(const QString& name,
- const QString& version,
- const QString& author,
- bool optionalFiles,
- InstancePtr instance,
- const QString& output,
- MMCZip::FilterFunction filter);
+ FlamePackExportTask(FlamePackExportOptions&& options);
protected:
void executeTask() override;
@@ -45,13 +49,6 @@ class FlamePackExportTask : public Task {
static const QStringList FILE_EXTENSIONS;
// inputs
- const QString name, version, author;
- const bool optionalFiles;
- const InstancePtr instance;
- MinecraftInstance* mcInstance;
- const QDir gameRoot;
- const QString output;
- const MMCZip::FilterFunction filter;
struct ResolvedFile {
int addonId;
@@ -70,6 +67,9 @@ class FlamePackExportTask : public Task {
bool isMod;
};
+ FlamePackExportOptions m_options;
+ QDir m_gameRoot;
+
FlameAPI api;
QFileInfoList files;
diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp
index 278105f4a..641fb5d9a 100644
--- a/launcher/modplatform/flame/PackManifest.cpp
+++ b/launcher/modplatform/flame/PackManifest.cpp
@@ -27,6 +27,7 @@ static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft)
loadModloaderV1(loader, obj);
m.modLoaders.append(loader);
}
+ m.recommendedRAM = Json::ensureInteger(minecraft, "recommendedRam", 0);
}
static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest)
diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h
index ebb3ed5cc..6b911ffb4 100644
--- a/launcher/modplatform/flame/PackManifest.h
+++ b/launcher/modplatform/flame/PackManifest.h
@@ -67,6 +67,7 @@ struct Minecraft {
QString version;
QString libraries;
QList modLoaders;
+ int recommendedRAM;
};
struct Manifest {
diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
index d103170af..4b19acd3f 100644
--- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
@@ -40,7 +40,7 @@ ModrinthPackExportTask::ModrinthPackExportTask(const QString& name,
bool optionalFiles,
InstancePtr instance,
const QString& output,
- MMCZip::FilterFunction filter)
+ MMCZip::FilterFileFunction filter)
: name(name)
, version(version)
, summary(summary)
@@ -63,7 +63,6 @@ bool ModrinthPackExportTask::abort()
{
if (task) {
task->abort();
- emitAborted();
return true;
}
return false;
@@ -158,6 +157,7 @@ void ModrinthPackExportTask::makeApiRequest()
task = api.currentVersions(pendingHashes.values(), "sha512", response);
connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response); });
connect(task.get(), &Task::failed, this, &ModrinthPackExportTask::emitFailed);
+ connect(task.get(), &Task::aborted, this, &ModrinthPackExportTask::emitAborted);
task->start();
}
}
diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h
index ee740a456..ec4730de5 100644
--- a/launcher/modplatform/modrinth/ModrinthPackExportTask.h
+++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h
@@ -35,7 +35,7 @@ class ModrinthPackExportTask : public Task {
bool optionalFiles,
InstancePtr instance,
const QString& output,
- MMCZip::FilterFunction filter);
+ MMCZip::FilterFileFunction filter);
protected:
void executeTask() override;
@@ -58,7 +58,7 @@ class ModrinthPackExportTask : public Task {
MinecraftInstance* mcInstance;
const QDir gameRoot;
const QString output;
- const MMCZip::FilterFunction filter;
+ const MMCZip::FilterFileFunction filter;
ModrinthAPI api;
QFileInfoList files;
diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h
index ac64052b9..f68230838 100644
--- a/launcher/net/ByteArraySink.h
+++ b/launcher/net/ByteArraySink.h
@@ -58,6 +58,7 @@ class ByteArraySink : public Sink {
qWarning() << "ByteArraySink did not initialize the buffer because it's not addressable";
if (initAllValidators(request))
return Task::State::Running;
+ m_fail_reason = "Failed to initialize validators";
return Task::State::Failed;
};
@@ -69,12 +70,14 @@ class ByteArraySink : public Sink {
qWarning() << "ByteArraySink did not write the buffer because it's not addressable";
if (writeAllValidators(data))
return Task::State::Running;
+ m_fail_reason = "Failed to write validators";
return Task::State::Failed;
}
auto abort() -> Task::State override
{
failAllValidators();
+ m_fail_reason = "Aborted";
return Task::State::Failed;
}
@@ -82,12 +85,13 @@ class ByteArraySink : public Sink {
{
if (finalizeAllValidators(reply))
return Task::State::Succeeded;
+ m_fail_reason = "Failed to finalize validators";
return Task::State::Failed;
}
auto hasLocalData() -> bool override { return false; }
- private:
+ protected:
std::shared_ptr m_output;
};
} // namespace Net
diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp
index 3a58a4667..1f519708f 100644
--- a/launcher/net/FileSink.cpp
+++ b/launcher/net/FileSink.cpp
@@ -51,6 +51,7 @@ Task::State FileSink::init(QNetworkRequest& request)
// create a new save file and open it for writing
if (!FS::ensureFilePathExists(m_filename)) {
qCCritical(taskNetLogC) << "Could not create folder for " + m_filename;
+ m_fail_reason = "Could not create folder";
return Task::State::Failed;
}
@@ -58,11 +59,13 @@ Task::State FileSink::init(QNetworkRequest& request)
m_output_file.reset(new PSaveFile(m_filename));
if (!m_output_file->open(QIODevice::WriteOnly)) {
qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing";
+ m_fail_reason = "Could not open file";
return Task::State::Failed;
}
if (initAllValidators(request))
return Task::State::Running;
+ m_fail_reason = "Failed to initialize validators";
return Task::State::Failed;
}
@@ -73,6 +76,7 @@ Task::State FileSink::write(QByteArray& data)
m_output_file->cancelWriting();
m_output_file.reset();
m_wroteAnyData = false;
+ m_fail_reason = "Failed to write validators";
return Task::State::Failed;
}
@@ -105,13 +109,16 @@ Task::State FileSink::finalize(QNetworkReply& reply)
if (gotFile || m_wroteAnyData) {
// ask validators for data consistency
// we only do this for actual downloads, not 'your data is still the same' cache hits
- if (!finalizeAllValidators(reply))
+ if (!finalizeAllValidators(reply)) {
+ m_fail_reason = "Failed to finalize validators";
return Task::State::Failed;
+ }
// nothing went wrong...
if (!m_output_file->commit()) {
qCCritical(taskNetLogC) << "Failed to commit changes to " << m_filename;
m_output_file->cancelWriting();
+ m_fail_reason = "Failed to commit changes";
return Task::State::Failed;
}
}
diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp
index 5a3a451b7..77c1de47d 100644
--- a/launcher/net/HttpMetaCache.cpp
+++ b/launcher/net/HttpMetaCache.cpp
@@ -166,7 +166,7 @@ auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool
return true;
}
-//returns true on success, false otherwise
+// returns true on success, false otherwise
auto HttpMetaCache::evictAll() -> bool
{
bool ret = true;
@@ -178,7 +178,7 @@ auto HttpMetaCache::evictAll() -> bool
qCWarning(taskHttpMetaCacheLogC) << "Unexpected missing cache entry" << entry->m_basePath;
}
map.entry_list.clear();
- //AND all return codes together so the result is true iff all runs of deletePath() are true
+ // AND all return codes together so the result is true iff all runs of deletePath() are true
ret &= FS::deletePath(map.base_path);
}
return ret;
diff --git a/launcher/net/NetRequest.cpp b/launcher/net/NetRequest.cpp
index ef533f599..7d8468a97 100644
--- a/launcher/net/NetRequest.cpp
+++ b/launcher/net/NetRequest.cpp
@@ -84,7 +84,8 @@ void NetRequest::executeTask()
break;
case State::Inactive:
case State::Failed:
- emit failed("Failed to initialize sink");
+ m_failReason = m_sink->failReason();
+ emit failed(m_sink->failReason());
emit finished();
return;
case State::AbortedByUser:
@@ -259,6 +260,7 @@ void NetRequest::downloadFinished()
} else if (m_state == State::Failed) {
qCDebug(logCat) << getUid().toString() << "Request failed in previous step:" << m_url.toString();
m_sink->abort();
+ m_failReason = m_reply->errorString();
emit failed(m_reply->errorString());
emit finished();
return;
@@ -278,7 +280,8 @@ void NetRequest::downloadFinished()
if (m_state != State::Succeeded) {
qCDebug(logCat) << getUid().toString() << "Request failed to write:" << m_url.toString();
m_sink->abort();
- emit failed("failed to write in sink");
+ m_failReason = m_sink->failReason();
+ emit failed(m_sink->failReason());
emit finished();
return;
}
@@ -289,7 +292,8 @@ void NetRequest::downloadFinished()
if (m_state != State::Succeeded) {
qCDebug(logCat) << getUid().toString() << "Request failed to finalize:" << m_url.toString();
m_sink->abort();
- emit failed("failed to finalize the request");
+ m_failReason = m_sink->failReason();
+ emit failed(m_sink->failReason());
emit finished();
return;
}
@@ -305,7 +309,7 @@ void NetRequest::downloadReadyRead()
auto data = m_reply->readAll();
m_state = m_sink->write(data);
if (m_state == State::Failed) {
- qCCritical(logCat) << getUid().toString() << "Failed to process response chunk";
+ qCCritical(logCat) << getUid().toString() << "Failed to process response chunk:" << m_sink->failReason();
}
// qDebug() << "Request" << m_url.toString() << "gained" << data.size() << "bytes";
} else {
diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp
index 86a44669e..0bbd077cf 100644
--- a/launcher/net/PasteUpload.cpp
+++ b/launcher/net/PasteUpload.cpp
@@ -36,74 +36,45 @@
*/
#include "PasteUpload.h"
-#include "Application.h"
-#include "BuildConfig.h"
+#include
-#include
-#include
#include
#include
#include
#include
+#include
#include
+#include "logs/AnonymizeLog.h"
-#include "net/Logging.h"
+const std::array PasteUpload::PasteTypes = { { { "0x0.st", "https://0x0.st", "" },
+ { "hastebin", "https://hst.sh", "/documents" },
+ { "paste.gg", "https://paste.gg", "/api/v1/pastes" },
+ { "mclo.gs", "https://api.mclo.gs", "/1/log" } } };
-std::array PasteUpload::PasteTypes = { { { "0x0.st", "https://0x0.st", "" },
- { "hastebin", "https://hst.sh", "/documents" },
- { "paste.gg", "https://paste.gg", "/api/v1/pastes" },
- { "mclo.gs", "https://api.mclo.gs", "/1/log" } } };
-
-PasteUpload::PasteUpload(QWidget* window, QString text, QString baseUrl, PasteType pasteType)
- : m_window(window), m_baseUrl(baseUrl), m_pasteType(pasteType), m_text(text.toUtf8())
+QNetworkReply* PasteUpload::getReply(QNetworkRequest& request)
{
- if (m_baseUrl == "")
- m_baseUrl = PasteTypes.at(pasteType).defaultBase;
-
- // HACK: Paste's docs say the standard API path is at /api/ but the official instance paste.gg doesn't follow that??
- if (pasteType == PasteGG && m_baseUrl == PasteTypes.at(pasteType).defaultBase)
- m_uploadUrl = "https://api.paste.gg/v1/pastes";
- else
- m_uploadUrl = m_baseUrl + PasteTypes.at(pasteType).endpointPath;
-}
-
-PasteUpload::~PasteUpload() {}
-
-void PasteUpload::executeTask()
-{
- QNetworkRequest request{ QUrl(m_uploadUrl) };
- QNetworkReply* rep{};
-
- request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8());
-
- switch (m_pasteType) {
- case NullPointer: {
- QHttpMultiPart* multiPart = new QHttpMultiPart{ QHttpMultiPart::FormDataType };
+ switch (m_paste_type) {
+ case PasteUpload::NullPointer: {
+ QHttpMultiPart* multiPart = new QHttpMultiPart{ QHttpMultiPart::FormDataType, this };
QHttpPart filePart;
- filePart.setBody(m_text);
+ filePart.setBody(m_log.toUtf8());
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain");
filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"file\"; filename=\"log.txt\"");
multiPart->append(filePart);
- rep = APPLICATION->network()->post(request, multiPart);
- multiPart->setParent(rep);
-
- break;
+ return m_network->post(request, multiPart);
}
- case Hastebin: {
- request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8());
- rep = APPLICATION->network()->post(request, m_text);
- break;
+ case PasteUpload::Hastebin: {
+ return m_network->post(request, m_log.toUtf8());
}
- case Mclogs: {
+ case PasteUpload::Mclogs: {
QUrlQuery postData;
- postData.addQueryItem("content", m_text);
+ postData.addQueryItem("content", m_log);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
- rep = APPLICATION->network()->post(request, postData.toString().toUtf8());
- break;
+ return m_network->post(request, postData.toString().toUtf8());
}
- case PasteGG: {
+ case PasteUpload::PasteGG: {
QJsonObject obj;
QJsonDocument doc;
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
@@ -114,7 +85,7 @@ void PasteUpload::executeTask()
QJsonObject logFileInfo;
QJsonObject logFileContentInfo;
logFileContentInfo.insert("format", "text");
- logFileContentInfo.insert("value", QString::fromUtf8(m_text));
+ logFileContentInfo.insert("value", m_log);
logFileInfo.insert("name", "log.txt");
logFileInfo.insert("content", logFileContentInfo);
files.append(logFileInfo);
@@ -122,108 +93,127 @@ void PasteUpload::executeTask()
obj.insert("files", files);
doc.setObject(obj);
- rep = APPLICATION->network()->post(request, doc.toJson());
- break;
+ return m_network->post(request, doc.toJson());
}
}
- connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress);
- connect(rep, &QNetworkReply::finished, this, &PasteUpload::downloadFinished);
+ return nullptr;
+};
- connect(rep, &QNetworkReply::errorOccurred, this, &PasteUpload::downloadError);
-
- m_reply = std::shared_ptr(rep);
-
- setStatus(tr("Uploading to %1").arg(m_uploadUrl));
-}
-
-void PasteUpload::downloadError(QNetworkReply::NetworkError error)
+auto PasteUpload::Sink::finalize(QNetworkReply& reply) -> Task::State
{
- // error happened during download.
- qCCritical(taskUploadLogC) << getUid().toString() << "Network error: " << error;
- emitFailed(m_reply->errorString());
-}
+ if (!finalizeAllValidators(reply)) {
+ m_fail_reason = "Failed to finalize validators";
+ return Task::State::Failed;
+ }
+ int statusCode = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-void PasteUpload::downloadFinished()
-{
- QByteArray data = m_reply->readAll();
- int statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (m_reply->error() != QNetworkReply::NetworkError::NoError) {
- emitFailed(tr("Network error: %1").arg(m_reply->errorString()));
- m_reply.reset();
- return;
+ if (reply.error() != QNetworkReply::NetworkError::NoError) {
+ m_fail_reason = QObject::tr("Network error: %1").arg(reply.errorString());
+ return Task::State::Failed;
} else if (statusCode != 200 && statusCode != 201) {
- QString reasonPhrase = m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
- emitFailed(tr("Error: %1 returned unexpected status code %2 %3").arg(m_uploadUrl).arg(statusCode).arg(reasonPhrase));
- qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned unexpected status code " << statusCode
- << " with body: " << data;
- m_reply.reset();
- return;
+ QString reasonPhrase = reply.attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
+ m_fail_reason =
+ QObject::tr("Error: %1 returned unexpected status code %2 %3").arg(m_d->url().toString()).arg(statusCode).arg(reasonPhrase);
+ return Task::State::Failed;
}
- switch (m_pasteType) {
- case NullPointer:
- m_pasteLink = QString::fromUtf8(data).trimmed();
+ switch (m_d->m_paste_type) {
+ case PasteUpload::NullPointer:
+ m_d->m_pasteLink = QString::fromUtf8(*m_output).trimmed();
break;
- case Hastebin: {
- QJsonDocument jsonDoc{ QJsonDocument::fromJson(data) };
- QJsonObject jsonObj{ jsonDoc.object() };
- if (jsonObj.contains("key") && jsonObj["key"].isString()) {
- QString key = jsonDoc.object()["key"].toString();
- m_pasteLink = m_baseUrl + "/" + key;
+ case PasteUpload::Hastebin: {
+ QJsonParseError jsonError;
+ auto doc = QJsonDocument::fromJson(*m_output, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError) {
+ qDebug() << "hastebin server did not reply with JSON" << jsonError.errorString();
+ m_fail_reason =
+ QObject::tr("Failed to parse response from hastebin server: expected JSON but got an invalid response. Error: %1")
+ .arg(jsonError.errorString());
+ return Task::State::Failed;
+ }
+ auto obj = doc.object();
+ if (obj.contains("key") && obj["key"].isString()) {
+ QString key = doc.object()["key"].toString();
+ m_d->m_pasteLink = m_d->m_baseUrl + "/" + key;
} else {
- emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl));
- qCCritical(taskUploadLogC) << getUid().toString() << getUid().toString() << m_uploadUrl
- << " returned malformed response body: " << data;
- return;
+ qDebug() << "Log upload failed:" << doc.toJson();
+ m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString());
+ return Task::State::Failed;
}
break;
}
- case Mclogs: {
- QJsonDocument jsonDoc{ QJsonDocument::fromJson(data) };
- QJsonObject jsonObj{ jsonDoc.object() };
- if (jsonObj.contains("success") && jsonObj["success"].isBool()) {
- bool success = jsonObj["success"].toBool();
+ case PasteUpload::Mclogs: {
+ QJsonParseError jsonError;
+ auto doc = QJsonDocument::fromJson(*m_output, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError) {
+ qDebug() << "mclogs server did not reply with JSON" << jsonError.errorString();
+ m_fail_reason =
+ QObject::tr("Failed to parse response from mclogs server: expected JSON but got an invalid response. Error: %1")
+ .arg(jsonError.errorString());
+ return Task::State::Failed;
+ }
+ auto obj = doc.object();
+ if (obj.contains("success") && obj["success"].isBool()) {
+ bool success = obj["success"].toBool();
if (success) {
- m_pasteLink = jsonObj["url"].toString();
+ m_d->m_pasteLink = obj["url"].toString();
} else {
- QString error = jsonObj["error"].toString();
- emitFailed(tr("Error: %1 returned an error: %2").arg(m_uploadUrl, error));
- qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned error: " << error;
- qCCritical(taskUploadLogC) << getUid().toString() << "Response body: " << data;
- return;
+ QString error = obj["error"].toString();
+ m_fail_reason = QObject::tr("Error: %1 returned an error: %2").arg(m_d->url().toString(), error);
+ return Task::State::Failed;
}
} else {
- emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl));
- qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned malformed response body: " << data;
- return;
+ qDebug() << "Log upload failed:" << doc.toJson();
+ m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString());
+ return Task::State::Failed;
}
break;
}
- case PasteGG:
- QJsonDocument jsonDoc{ QJsonDocument::fromJson(data) };
- QJsonObject jsonObj{ jsonDoc.object() };
- if (jsonObj.contains("status") && jsonObj["status"].isString()) {
- QString status = jsonObj["status"].toString();
+ case PasteUpload::PasteGG:
+ QJsonParseError jsonError;
+ auto doc = QJsonDocument::fromJson(*m_output, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError) {
+ qDebug() << "pastegg server did not reply with JSON" << jsonError.errorString();
+ m_fail_reason =
+ QObject::tr("Failed to parse response from pasteGG server: expected JSON but got an invalid response. Error: %1")
+ .arg(jsonError.errorString());
+ return Task::State::Failed;
+ }
+ auto obj = doc.object();
+ if (obj.contains("status") && obj["status"].isString()) {
+ QString status = obj["status"].toString();
if (status == "success") {
- m_pasteLink = m_baseUrl + "/p/anonymous/" + jsonObj["result"].toObject()["id"].toString();
+ m_d->m_pasteLink = m_d->m_baseUrl + "/p/anonymous/" + obj["result"].toObject()["id"].toString();
} else {
- QString error = jsonObj["error"].toString();
- QString message =
- (jsonObj.contains("message") && jsonObj["message"].isString()) ? jsonObj["message"].toString() : "none";
- emitFailed(tr("Error: %1 returned an error code: %2\nError message: %3").arg(m_uploadUrl, error, message));
- qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned error: " << error;
- qCCritical(taskUploadLogC) << getUid().toString() << "Error message: " << message;
- qCCritical(taskUploadLogC) << getUid().toString() << "Response body: " << data;
- return;
+ QString error = obj["error"].toString();
+ QString message = (obj.contains("message") && obj["message"].isString()) ? obj["message"].toString() : "none";
+ m_fail_reason =
+ QObject::tr("Error: %1 returned an error code: %2\nError message: %3").arg(m_d->url().toString(), error, message);
+ return Task::State::Failed;
}
} else {
- emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl));
- qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned malformed response body: " << data;
- return;
+ qDebug() << "Log upload failed:" << doc.toJson();
+ m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString());
+ return Task::State::Failed;
}
break;
}
- emitSucceeded();
+ return Task::State::Succeeded;
+}
+
+PasteUpload::PasteUpload(const QString& log, QString url, PasteType pasteType) : m_log(log), m_baseUrl(url), m_paste_type(pasteType)
+{
+ anonymizeLog(m_log);
+ auto base = PasteUpload::PasteTypes.at(pasteType);
+ if (m_baseUrl.isEmpty())
+ m_baseUrl = base.defaultBase;
+
+ // HACK: Paste's docs say the standard API path is at /api/ but the official instance paste.gg doesn't follow that??
+ if (pasteType == PasteUpload::PasteGG && m_baseUrl == base.defaultBase)
+ m_url = "https://api.paste.gg/v1/pastes";
+ else
+ m_url = m_baseUrl + base.endpointPath;
+
+ m_sink.reset(new Sink(this));
}
diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h
index 2ba6067c3..7f43779c4 100644
--- a/launcher/net/PasteUpload.h
+++ b/launcher/net/PasteUpload.h
@@ -35,15 +35,18 @@
#pragma once
-#include
-#include
-#include
-#include
-#include
+#include "net/ByteArraySink.h"
+#include "net/NetRequest.h"
#include "tasks/Task.h"
-class PasteUpload : public Task {
- Q_OBJECT
+#include
+#include
+#include
+
+#include
+#include
+
+class PasteUpload : public Net::NetRequest {
public:
enum PasteType : int {
// 0x0.st
@@ -58,32 +61,36 @@ class PasteUpload : public Task {
First = NullPointer,
Last = Mclogs
};
-
struct PasteTypeInfo {
const QString name;
const QString defaultBase;
const QString endpointPath;
};
- static std::array PasteTypes;
+ static const std::array PasteTypes;
- PasteUpload(QWidget* window, QString text, QString url, PasteType pasteType);
- virtual ~PasteUpload();
+ class Sink : public Net::ByteArraySink {
+ public:
+ Sink(PasteUpload* p) : Net::ByteArraySink(std::make_shared()), m_d(p) {};
+ virtual ~Sink() = default;
+
+ public:
+ auto finalize(QNetworkReply& reply) -> Task::State override;
+
+ private:
+ PasteUpload* m_d;
+ };
+ friend Sink;
+
+ PasteUpload(const QString& log, QString url, PasteType pasteType);
+ virtual ~PasteUpload() = default;
QString pasteLink() { return m_pasteLink; }
- protected:
- virtual void executeTask();
-
private:
- QWidget* m_window;
+ virtual QNetworkReply* getReply(QNetworkRequest&) override;
+ QString m_log;
QString m_pasteLink;
QString m_baseUrl;
- QString m_uploadUrl;
- PasteType m_pasteType;
- QByteArray m_text;
- std::shared_ptr m_reply;
- public slots:
- void downloadError(QNetworkReply::NetworkError);
- void downloadFinished();
+ const PasteType m_paste_type;
};
diff --git a/launcher/net/Sink.h b/launcher/net/Sink.h
index d1fd9de10..3f04cbd82 100644
--- a/launcher/net/Sink.h
+++ b/launcher/net/Sink.h
@@ -52,6 +52,8 @@ class Sink {
virtual auto hasLocalData() -> bool = 0;
+ QString failReason() const { return m_fail_reason; }
+
void addValidator(Validator* validator)
{
if (validator) {
@@ -95,5 +97,6 @@ class Sink {
protected:
std::vector> validators;
+ QString m_fail_reason;
};
} // namespace Net
diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp
index 7ee98760a..1355c74c0 100644
--- a/launcher/screenshots/ImgurAlbumCreation.cpp
+++ b/launcher/screenshots/ImgurAlbumCreation.cpp
@@ -86,6 +86,7 @@ auto ImgurAlbumCreation::Sink::write(QByteArray& data) -> Task::State
auto ImgurAlbumCreation::Sink::abort() -> Task::State
{
m_output.clear();
+ m_fail_reason = "Aborted";
return Task::State::Failed;
}
@@ -95,11 +96,13 @@ auto ImgurAlbumCreation::Sink::finalize(QNetworkReply&) -> Task::State
QJsonDocument doc = QJsonDocument::fromJson(m_output, &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
qDebug() << jsonError.errorString();
+ m_fail_reason = "Invalid json reply";
return Task::State::Failed;
}
auto object = doc.object();
if (!object.value("success").toBool()) {
qDebug() << doc.toJson();
+ m_fail_reason = "Failed to create album";
return Task::State::Failed;
}
m_result->deleteHash = object.value("data").toObject().value("deletehash").toString();
diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp
index 8b4ef5327..835a1ab81 100644
--- a/launcher/screenshots/ImgurUpload.cpp
+++ b/launcher/screenshots/ImgurUpload.cpp
@@ -90,6 +90,7 @@ auto ImgurUpload::Sink::write(QByteArray& data) -> Task::State
auto ImgurUpload::Sink::abort() -> Task::State
{
m_output.clear();
+ m_fail_reason = "Aborted";
return Task::State::Failed;
}
@@ -99,11 +100,13 @@ auto ImgurUpload::Sink::finalize(QNetworkReply&) -> Task::State
QJsonDocument doc = QJsonDocument::fromJson(m_output, &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
qDebug() << "imgur server did not reply with JSON" << jsonError.errorString();
+ m_fail_reason = "Invalid json reply";
return Task::State::Failed;
}
auto object = doc.object();
if (!object.value("success").toBool()) {
qDebug() << "Screenshot upload not successful:" << doc.toJson();
+ m_fail_reason = "Screenshot was not uploaded successfully";
return Task::State::Failed;
}
m_shot->m_imgurId = object.value("data").toObject().value("id").toString();
diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp
index ad2a14c42..84530ec99 100644
--- a/launcher/tasks/ConcurrentTask.cpp
+++ b/launcher/tasks/ConcurrentTask.cpp
@@ -118,10 +118,29 @@ void ConcurrentTask::executeNextSubTask()
}
if (m_queue.isEmpty()) {
if (m_doing.isEmpty()) {
- if (m_failed.isEmpty())
+ if (m_failed.isEmpty()) {
emitSucceeded();
- else
- emitFailed(tr("One or more subtasks failed"));
+ } else if (m_failed.count() == 1) {
+ auto task = m_failed.keys().first();
+ auto reason = task->failReason();
+ if (reason.isEmpty()) { // clearly a bug somewhere
+ reason = tr("Task failed");
+ }
+ emitFailed(reason);
+ } else {
+ QStringList failReason;
+ for (auto t : m_failed) {
+ auto reason = t->failReason();
+ if (!reason.isEmpty()) {
+ failReason << reason;
+ }
+ }
+ if (failReason.isEmpty()) {
+ emitFailed(tr("Multiple subtasks failed"));
+ } else {
+ emitFailed(tr("Multiple subtasks failed\n%1").arg(failReason.join("\n")));
+ }
+ }
}
return;
}
diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp
index 1871c5df8..92b345c8d 100644
--- a/launcher/tasks/Task.cpp
+++ b/launcher/tasks/Task.cpp
@@ -196,6 +196,8 @@ void Task::logWarning(const QString& line)
{
qWarning() << line;
m_Warnings.append(line);
+
+ emit warningLogged(line);
}
QStringList Task::warnings() const
diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h
index 503d6a6b6..43e71c8ab 100644
--- a/launcher/tasks/Task.h
+++ b/launcher/tasks/Task.h
@@ -79,7 +79,6 @@ Q_DECLARE_METATYPE(TaskStepProgress)
using TaskStepProgressList = QList>;
-
/*!
* Represents a task that has to be done.
* To create a task, you need to subclass this class, implement the executeTask() method and call
@@ -146,6 +145,7 @@ class Task : public QObject, public QRunnable {
void failed(QString reason);
void status(QString status);
void details(QString details);
+ void warningLogged(const QString& warning);
void stepProgress(TaskStepProgress const& task_progress);
//! Emitted when the canAbort() status has changed. */
@@ -177,9 +177,9 @@ class Task : public QObject, public QRunnable {
virtual void executeTask() = 0;
protected slots:
- //! The Task subclass must call this method when the task has succeeded
+ //! The Task subclass must call this method when the task has succeeded
virtual void emitSucceeded();
- //! **The Task subclass** must call this method when the task has aborted. External code should call abort() instead.
+ //! **The Task subclass** must call this method when the task has aborted. External code should call abort() instead.
virtual void emitAborted();
//! The Task subclass must call this method when the task has failed
virtual void emitFailed(QString reason = "");
diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp
index d53ade86d..141153b92 100644
--- a/launcher/ui/GuiUtil.cpp
+++ b/launcher/ui/GuiUtil.cpp
@@ -38,10 +38,15 @@
#include "GuiUtil.h"
#include
+#include
#include
#include
#include
+#include "FileSystem.h"
+#include "logs/AnonymizeLog.h"
+#include "net/NetJob.h"
+#include "net/NetRequest.h"
#include "net/PasteUpload.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h"
@@ -74,52 +79,52 @@ QString truncateLogForMclogs(const QString& logContent)
return logContent;
}
+std::optional GuiUtil::uploadPaste(const QString& name, const QFileInfo& filePath, QWidget* parentWidget)
+{
+ return uploadPaste(name, FS::read(filePath.absoluteFilePath()), parentWidget);
+};
+
std::optional GuiUtil::uploadPaste(const QString& name, const QString& text, QWidget* parentWidget)
{
ProgressDialog dialog(parentWidget);
- auto pasteTypeSetting = static_cast(APPLICATION->settings()->get("PastebinType").toInt());
- auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString();
+ auto pasteType = static_cast(APPLICATION->settings()->get("PastebinType").toInt());
+ auto baseURL = APPLICATION->settings()->get("PastebinCustomAPIBase").toString();
bool shouldTruncate = false;
- {
- QUrl baseUrl;
- if (pasteCustomAPIBaseSetting.isEmpty())
- baseUrl = PasteUpload::PasteTypes[pasteTypeSetting].defaultBase;
- else
- baseUrl = pasteCustomAPIBaseSetting;
+ if (baseURL.isEmpty())
+ baseURL = PasteUpload::PasteTypes[pasteType].defaultBase;
- if (baseUrl.isValid()) {
- auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"),
- QObject::tr("You are about to upload \"%1\" to %2.\n"
- "You should double-check for personal information.\n\n"
- "Are you sure?")
- .arg(name, baseUrl.host()),
- QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
- ->exec();
+ if (auto url = QUrl(baseURL); url.isValid()) {
+ auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"),
+ QObject::tr("You are about to upload \"%1\" to %2.\n"
+ "You should double-check for personal information.\n\n"
+ "Are you sure?")
+ .arg(name, url.host()),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
- if (response != QMessageBox::Yes)
+ if (response != QMessageBox::Yes)
+ return {};
+
+ if (baseURL == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) {
+ auto truncateResponse = CustomMessageBox::selectable(
+ parentWidget, QObject::tr("Confirm Truncation"),
+ QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n"
+ "The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n"
+ "If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off "
+ "potentially useful info like crashes at the end.\n\n"
+ "Proceed with truncation?")
+ .arg(text.count("\n"))
+ .arg(MaxMclogsLines)
+ .arg(InitialMclogsLines)
+ .arg(FinalMclogsLines),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No)
+ ->exec();
+
+ if (truncateResponse == QMessageBox::Cancel) {
return {};
-
- if (baseUrl.toString() == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) {
- auto truncateResponse = CustomMessageBox::selectable(
- parentWidget, QObject::tr("Confirm Truncation"),
- QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n"
- "The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n"
- "If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off "
- "potentially useful info like crashes at the end.\n\n"
- "Proceed with truncation?")
- .arg(text.count("\n"))
- .arg(MaxMclogsLines)
- .arg(InitialMclogsLines)
- .arg(FinalMclogsLines),
- QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No)
- ->exec();
-
- if (truncateResponse == QMessageBox::Cancel) {
- return {};
- }
- shouldTruncate = truncateResponse == QMessageBox::Yes;
}
+ shouldTruncate = truncateResponse == QMessageBox::Yes;
}
}
@@ -128,26 +133,40 @@ std::optional GuiUtil::uploadPaste(const QString& name, const QString&
textToUpload = truncateLogForMclogs(text);
}
- std::unique_ptr paste(new PasteUpload(parentWidget, textToUpload, pasteCustomAPIBaseSetting, pasteTypeSetting));
+ auto job = NetJob::Ptr(new NetJob("Log Upload", APPLICATION->network()));
- dialog.execWithTask(paste.get());
- if (!paste->wasSuccessful()) {
- CustomMessageBox::selectable(parentWidget, QObject::tr("Upload failed"), paste->failReason(), QMessageBox::Critical)->exec();
- return QString();
- } else {
- const QString link = paste->pasteLink();
- setClipboardText(link);
+ auto pasteJob = new PasteUpload(textToUpload, baseURL, pasteType);
+ job->addNetAction(Net::NetRequest::Ptr(pasteJob));
+ QObject::connect(job.get(), &Task::failed, [parentWidget](QString reason) {
+ CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), reason, QMessageBox::Critical)->show();
+ });
+ QObject::connect(job.get(), &Task::aborted, [parentWidget] {
+ CustomMessageBox::selectable(parentWidget, QObject::tr("Logs upload aborted"),
+ QObject::tr("The task has been aborted by the user."), QMessageBox::Information)
+ ->show();
+ });
+
+ if (dialog.execWithTask(job.get()) == QDialog::Accepted) {
+ if (pasteJob->pasteLink().isEmpty()) {
+ CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), "The upload link is empty",
+ QMessageBox::Critical)
+ ->show();
+ return {};
+ }
+ setClipboardText(pasteJob->pasteLink());
CustomMessageBox::selectable(
parentWidget, QObject::tr("Upload finished"),
- QObject::tr("The link to the uploaded log has been placed in your clipboard.").arg(link),
+ QObject::tr("The link to the uploaded log has been placed in your clipboard.").arg(pasteJob->pasteLink()),
QMessageBox::Information)
->exec();
- return link;
+ return pasteJob->pasteLink();
}
+ return {};
}
-void GuiUtil::setClipboardText(const QString& text)
+void GuiUtil::setClipboardText(QString text)
{
+ anonymizeLog(text);
QApplication::clipboard()->setText(text);
}
diff --git a/launcher/ui/GuiUtil.h b/launcher/ui/GuiUtil.h
index 8d384d3f6..c3ba01f5b 100644
--- a/launcher/ui/GuiUtil.h
+++ b/launcher/ui/GuiUtil.h
@@ -1,11 +1,13 @@
#pragma once
+#include
#include
#include
namespace GuiUtil {
-std::optional uploadPaste(const QString& name, const QString& text, QWidget* parentWidget);
-void setClipboardText(const QString& text);
+std::optional uploadPaste(const QString& name, const QFileInfo& filePath, QWidget* parentWidget);
+std::optional uploadPaste(const QString& name, const QString& data, QWidget* parentWidget);
+void setClipboardText(QString text);
QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget);
QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget);
} // namespace GuiUtil
diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp
index d190c6a02..63fcc3c9b 100644
--- a/launcher/ui/MainWindow.cpp
+++ b/launcher/ui/MainWindow.cpp
@@ -71,6 +71,7 @@
#include
#include
#include
+#include
#include
#include
@@ -90,8 +91,10 @@
#include
#include "InstanceWindow.h"
+#include "ui/GuiUtil.h"
#include "ui/dialogs/AboutDialog.h"
#include "ui/dialogs/CopyInstanceDialog.h"
+#include "ui/dialogs/CreateShortcutDialog.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ExportInstanceDialog.h"
#include "ui/dialogs/ExportPackDialog.h"
@@ -235,6 +238,16 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi
ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED);
}
+ { // logs upload
+
+ auto menu = new QMenu(this);
+ for (auto file : QDir("logs").entryInfoList(QDir::Files)) {
+ auto action = menu->addAction(file.fileName());
+ connect(action, &QAction::triggered, this, [this, file] { GuiUtil::uploadPaste(file.fileName(), file, this); });
+ }
+ ui->actionUploadLog->setMenu(menu);
+ }
+
// add the toolbar toggles to the view menu
ui->viewMenu->addAction(ui->instanceToolBar->toggleViewAction());
ui->viewMenu->addAction(ui->newsToolBar->toggleViewAction());
@@ -1311,7 +1324,7 @@ void MainWindow::on_actionReportBug_triggered()
void MainWindow::on_actionClearMetadata_triggered()
{
- //This if contains side effects!
+ // This if contains side effects!
if (!APPLICATION->metacache()->evictAll()) {
CustomMessageBox::selectable(this, tr("Error"),
tr("Metadata cache clear Failed!\nTo clear the metadata cache manually, press Folders -> View "
@@ -1383,6 +1396,14 @@ void MainWindow::on_actionDeleteInstance_triggered()
return;
}
+ if (m_selectedInstance->isRunning()) {
+ CustomMessageBox::selectable(this, tr("Cannot Delete Running Instance"),
+ tr("The selected instance is currently running and cannot be deleted. Please stop the instance before "
+ "attempting to delete it."),
+ QMessageBox::Warning, QMessageBox::Ok)
+ ->exec();
+ return;
+ }
auto id = m_selectedInstance->id();
auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"),
@@ -1419,15 +1440,18 @@ void MainWindow::on_actionExportInstanceZip_triggered()
void MainWindow::on_actionExportInstanceMrPack_triggered()
{
if (m_selectedInstance) {
- ExportPackDialog dlg(m_selectedInstance, this);
- dlg.exec();
+ auto instance = std::dynamic_pointer_cast(m_selectedInstance);
+ if (instance != nullptr) {
+ ExportPackDialog dlg(instance, this);
+ dlg.exec();
+ }
}
}
void MainWindow::on_actionExportInstanceFlamePack_triggered()
{
if (m_selectedInstance) {
- auto instance = dynamic_cast(m_selectedInstance.get());
+ auto instance = std::dynamic_pointer_cast(m_selectedInstance);
if (instance) {
if (auto cmp = instance->getPackProfile()->getComponent("net.minecraft");
cmp && cmp->getVersionFile() && cmp->getVersionFile()->type == "snapshot") {
@@ -1436,7 +1460,7 @@ void MainWindow::on_actionExportInstanceFlamePack_triggered()
msgBox.exec();
return;
}
- ExportPackDialog dlg(m_selectedInstance, this, ModPlatform::ResourceProvider::FLAME);
+ ExportPackDialog dlg(instance, this, ModPlatform::ResourceProvider::FLAME);
dlg.exec();
}
}
@@ -1510,139 +1534,11 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered()
{
if (!m_selectedInstance)
return;
- auto desktopPath = FS::getDesktopDir();
- if (desktopPath.isEmpty()) {
- // TODO come up with an alternative solution (open "save file" dialog)
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!"));
+
+ CreateShortcutDialog shortcutDlg(m_selectedInstance, this);
+ if (!shortcutDlg.exec())
return;
- }
-
- QString desktopFilePath;
- QString appPath = QApplication::applicationFilePath();
- QString iconPath;
- QStringList args;
-#if defined(Q_OS_MACOS)
- appPath = QApplication::applicationFilePath();
- if (appPath.startsWith("/private/var/")) {
- QMessageBox::critical(this, tr("Create instance shortcut"),
- tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts."));
- return;
- }
-
- auto pIcon = APPLICATION->icons()->icon(m_selectedInstance->iconKey());
- if (pIcon == nullptr) {
- pIcon = APPLICATION->icons()->icon("grass");
- }
-
- iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "Icon.icns");
-
- QFile iconFile(iconPath);
- if (!iconFile.open(QFile::WriteOnly)) {
- QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application."));
- return;
- }
-
- QIcon icon = pIcon->icon();
-
- bool success = icon.pixmap(1024, 1024).save(iconPath, "ICNS");
- iconFile.close();
-
- if (!success) {
- iconFile.remove();
- QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application."));
- return;
- }
-#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
- if (appPath.startsWith("/tmp/.mount_")) {
- // AppImage!
- appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE"));
- if (appPath.isEmpty()) {
- QMessageBox::critical(this, tr("Create instance shortcut"),
- tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)"));
- } else if (appPath.endsWith("/")) {
- appPath.chop(1);
- }
- }
-
- auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey());
- if (icon == nullptr) {
- icon = APPLICATION->icons()->icon("grass");
- }
-
- iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png");
-
- QFile iconFile(iconPath);
- if (!iconFile.open(QFile::WriteOnly)) {
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
- return;
- }
- bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG");
- iconFile.close();
-
- if (!success) {
- iconFile.remove();
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
- return;
- }
-
- if (DesktopServices::isFlatpak()) {
- desktopFilePath = FS::PathCombine(desktopPath, FS::RemoveInvalidFilenameChars(m_selectedInstance->name()) + ".desktop");
- QFileDialog fileDialog;
- // workaround to make sure the portal file dialog opens in the desktop directory
- fileDialog.setDirectoryUrl(desktopPath);
- desktopFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), desktopFilePath, tr("Desktop Entries") + " (*.desktop)");
- if (desktopFilePath.isEmpty())
- return; // file dialog canceled by user
- appPath = "flatpak";
- args.append({ "run", BuildConfig.LAUNCHER_APPID });
- }
-
-#elif defined(Q_OS_WIN)
- auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey());
- if (icon == nullptr) {
- icon = APPLICATION->icons()->icon("grass");
- }
-
- iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico");
-
- // part of fix for weird bug involving the window icon being replaced
- // dunno why it happens, but this 2-line fix seems to be enough, so w/e
- auto appIcon = APPLICATION->getThemedIcon("logo");
-
- QFile iconFile(iconPath);
- if (!iconFile.open(QFile::WriteOnly)) {
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
- return;
- }
- bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO");
- iconFile.close();
-
- // restore original window icon
- QGuiApplication::setWindowIcon(appIcon);
-
- if (!success) {
- iconFile.remove();
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
- return;
- }
-
-#else
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!"));
- return;
-#endif
- args.append({ "--launch", m_selectedInstance->id() });
- if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) {
-#if not defined(Q_OS_MACOS)
- QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!"));
-#else
- QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!"));
-#endif
- } else {
-#if not defined(Q_OS_MACOS)
- iconFile.remove();
-#endif
- QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!"));
- }
+ shortcutDlg.createShortcut();
}
void MainWindow::taskEnd()
diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui
index f20c34206..1499ec872 100644
--- a/launcher/ui/MainWindow.ui
+++ b/launcher/ui/MainWindow.ui
@@ -131,7 +131,7 @@
0
0
800
- 27
+ 22
-
- ..
+
More news...
@@ -250,8 +250,7 @@
true
-
- ..
+
&Meow
@@ -286,8 +285,7 @@
-
- ..
+
Add Instanc&e...
@@ -298,8 +296,7 @@
-
- ..
+
&Update...
@@ -313,8 +310,7 @@
-
- ..
+
Setti&ngs...
@@ -328,8 +324,7 @@
-
- ..
+
&Manage Accounts...
@@ -337,8 +332,7 @@
-
- ..
+
&Launch
@@ -349,8 +343,7 @@
-
- ..
+
&Kill
@@ -364,8 +357,7 @@
-
- ..
+
Rename
@@ -376,8 +368,7 @@
-
- ..
+
&Change Group...
@@ -399,8 +390,7 @@
-
- ..
+
&Edit...
@@ -414,8 +404,7 @@
-
- ..
+
&Folder
@@ -426,8 +415,7 @@
-
- ..
+
Dele&te
@@ -441,8 +429,7 @@
-
- ..
+
Cop&y...
@@ -456,8 +443,7 @@
-
- ..
+
E&xport...
@@ -468,8 +454,7 @@
-
- ..
+
Prism Launcher (zip)
@@ -477,8 +462,7 @@
-
- ..
+
Modrinth (mrpack)
@@ -486,8 +470,7 @@
-
- ..
+
CurseForge (zip)
@@ -495,20 +478,18 @@
-
- ..
+
Create Shortcut
- Creates a shortcut on your desktop to launch the selected instance.
+ Creates a shortcut on a selected folder to launch the selected instance.
-
- ..
+
No accounts added!
@@ -519,8 +500,7 @@
true
-
- ..
+
No Default Account
@@ -531,8 +511,7 @@
-
- ..
+
Close &Window
@@ -546,8 +525,7 @@
-
- ..
+
&Instances
@@ -558,8 +536,7 @@
-
- ..
+
Launcher &Root
@@ -570,8 +547,7 @@
-
- ..
+
&Central Mods
@@ -582,8 +558,7 @@
-
- ..
+
&Skins
@@ -594,8 +569,7 @@
-
- ..
+
Instance Icons
@@ -606,8 +580,7 @@
-
- ..
+
Logs
@@ -623,8 +596,7 @@
-
- ..
+
Report a Bug or Suggest a Feature
@@ -635,8 +607,7 @@
-
- ..
+
&Discord Guild
@@ -647,8 +618,7 @@
-
- ..
+
&Matrix Space
@@ -659,8 +629,7 @@
-
- ..
+
Sub&reddit
@@ -671,8 +640,7 @@
-
- ..
+
&About %1
@@ -686,8 +654,7 @@
-
- ..
+
&Clear Metadata Cache
@@ -696,10 +663,21 @@
Clear cached metadata
+
+
+
+ ..
+
+
+ Upload logs
+
+
+ Upload launcher logs to the selected log provider
+
+
-
- ..
+
Install to &PATH
@@ -710,8 +688,7 @@
-
- ..
+
Folders
@@ -722,8 +699,7 @@
-
- ..
+
Help
@@ -734,8 +710,7 @@
-
- ..
+
Accounts
@@ -743,8 +718,7 @@
-
- ..
+
%1 &Help
@@ -755,8 +729,7 @@
-
- ..
+
&Widget Themes
@@ -767,8 +740,7 @@
-
- ..
+
I&con Theme
@@ -779,8 +751,7 @@
-
- ..
+
Cat Packs
@@ -791,8 +762,7 @@
-
- ..
+
Java
diff --git a/launcher/ui/dialogs/CreateShortcutDialog.cpp b/launcher/ui/dialogs/CreateShortcutDialog.cpp
new file mode 100644
index 000000000..278573a22
--- /dev/null
+++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp
@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu
+ * Copyright (C) 2023 TheKodeToad
+ * Copyright (C) 2025 Yihe Li
+ *
+ * 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 "Application.h"
+#include "BuildConfig.h"
+#include "CreateShortcutDialog.h"
+#include "ui_CreateShortcutDialog.h"
+
+#include "ui/dialogs/IconPickerDialog.h"
+
+#include "BaseInstance.h"
+#include "DesktopServices.h"
+#include "FileSystem.h"
+#include "InstanceList.h"
+#include "icons/IconList.h"
+
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/ShortcutUtils.h"
+#include "minecraft/WorldList.h"
+#include "minecraft/auth/AccountList.h"
+
+CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent)
+ : QDialog(parent), ui(new Ui::CreateShortcutDialog), m_instance(instance)
+{
+ ui->setupUi(this);
+
+ InstIconKey = instance->iconKey();
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
+ ui->instNameTextBox->setPlaceholderText(instance->name());
+
+ auto mInst = std::dynamic_pointer_cast(instance);
+ m_QuickJoinSupported = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer");
+ auto worldList = mInst->worldList();
+ worldList->update();
+ if (!m_QuickJoinSupported || worldList->empty()) {
+ ui->worldTarget->hide();
+ ui->worldSelectionBox->hide();
+ ui->serverTarget->setChecked(true);
+ ui->serverTarget->hide();
+ ui->serverLabel->show();
+ }
+
+ // Populate save targets
+ if (!DesktopServices::isFlatpak()) {
+ QString desktopDir = FS::getDesktopDir();
+ QString applicationDir = FS::getApplicationsDir();
+
+ if (!desktopDir.isEmpty())
+ ui->saveTargetSelectionBox->addItem(tr("Desktop"), QVariant::fromValue(SaveTarget::Desktop));
+
+ if (!applicationDir.isEmpty())
+ ui->saveTargetSelectionBox->addItem(tr("Applications"), QVariant::fromValue(SaveTarget::Applications));
+ }
+ ui->saveTargetSelectionBox->addItem(tr("Other..."), QVariant::fromValue(SaveTarget::Other));
+
+ // Populate worlds
+ if (m_QuickJoinSupported) {
+ for (const auto& world : worldList->allWorlds()) {
+ // Entry name: World Name [Game Mode] - Last Played: DateTime
+ QString entry_name = tr("%1 [%2] - Last Played: %3")
+ .arg(world.name(), world.gameType().toTranslatedString(), world.lastPlayed().toString(Qt::ISODate));
+ ui->worldSelectionBox->addItem(entry_name, world.name());
+ }
+ }
+
+ // Populate accounts
+ auto accounts = APPLICATION->accounts();
+ MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
+ if (accounts->count() <= 0) {
+ ui->overrideAccountCheckbox->setEnabled(false);
+ } else {
+ for (int i = 0; i < accounts->count(); i++) {
+ MinecraftAccountPtr account = accounts->at(i);
+ auto profileLabel = account->profileName();
+ if (account->isInUse())
+ profileLabel = tr("%1 (in use)").arg(profileLabel);
+ auto face = account->getFace();
+ QIcon icon = face.isNull() ? APPLICATION->getThemedIcon("noaccount") : face;
+ ui->accountSelectionBox->addItem(profileLabel, account->profileName());
+ ui->accountSelectionBox->setItemIcon(i, icon);
+ if (defaultAccount == account)
+ ui->accountSelectionBox->setCurrentIndex(i);
+ }
+ }
+}
+
+CreateShortcutDialog::~CreateShortcutDialog()
+{
+ delete ui;
+}
+
+void CreateShortcutDialog::on_iconButton_clicked()
+{
+ IconPickerDialog dlg(this);
+ dlg.execWithSelection(InstIconKey);
+
+ if (dlg.result() == QDialog::Accepted) {
+ InstIconKey = dlg.selectedIconKey;
+ ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
+ }
+}
+
+void CreateShortcutDialog::on_overrideAccountCheckbox_stateChanged(int state)
+{
+ ui->accountOptionsGroup->setEnabled(state == Qt::Checked);
+}
+
+void CreateShortcutDialog::on_targetCheckbox_stateChanged(int state)
+{
+ ui->targetOptionsGroup->setEnabled(state == Qt::Checked);
+ ui->worldSelectionBox->setEnabled(ui->worldTarget->isChecked());
+ ui->serverAddressBox->setEnabled(ui->serverTarget->isChecked());
+ stateChanged();
+}
+
+void CreateShortcutDialog::on_worldTarget_toggled(bool checked)
+{
+ ui->worldSelectionBox->setEnabled(checked);
+ stateChanged();
+}
+
+void CreateShortcutDialog::on_serverTarget_toggled(bool checked)
+{
+ ui->serverAddressBox->setEnabled(checked);
+ stateChanged();
+}
+
+void CreateShortcutDialog::on_worldSelectionBox_currentIndexChanged(int index)
+{
+ stateChanged();
+}
+
+void CreateShortcutDialog::on_serverAddressBox_textChanged(const QString& text)
+{
+ stateChanged();
+}
+
+void CreateShortcutDialog::stateChanged()
+{
+ QString result = m_instance->name();
+ if (ui->targetCheckbox->isChecked()) {
+ if (ui->worldTarget->isChecked())
+ result = tr("%1 - %2").arg(result, ui->worldSelectionBox->currentData().toString());
+ else if (ui->serverTarget->isChecked())
+ result = tr("%1 - Server %2").arg(result, ui->serverAddressBox->text());
+ }
+ ui->instNameTextBox->setPlaceholderText(result);
+ if (!ui->targetCheckbox->isChecked())
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
+ else {
+ ui->buttonBox->button(QDialogButtonBox::Ok)
+ ->setEnabled((ui->worldTarget->isChecked() && ui->worldSelectionBox->currentIndex() != -1) ||
+ (ui->serverTarget->isChecked() && !ui->serverAddressBox->text().isEmpty()));
+ }
+}
+
+// Real work
+void CreateShortcutDialog::createShortcut()
+{
+ QString targetString = tr("instance");
+ QStringList extraArgs;
+ if (ui->targetCheckbox->isChecked()) {
+ if (ui->worldTarget->isChecked()) {
+ targetString = tr("world");
+ extraArgs = { "--world", ui->worldSelectionBox->currentData().toString() };
+ } else if (ui->serverTarget->isChecked()) {
+ targetString = tr("server");
+ extraArgs = { "--server", ui->serverAddressBox->text() };
+ }
+ }
+
+ auto target = ui->saveTargetSelectionBox->currentData().value();
+ auto name = ui->instNameTextBox->text();
+ if (name.isEmpty())
+ name = ui->instNameTextBox->placeholderText();
+ if (ui->overrideAccountCheckbox->isChecked())
+ extraArgs.append({ "--profile", ui->accountSelectionBox->currentData().toString() });
+
+ ShortcutUtils::Shortcut args{ m_instance.get(), name, targetString, this, extraArgs, InstIconKey };
+ if (target == SaveTarget::Desktop)
+ ShortcutUtils::createInstanceShortcutOnDesktop(args);
+ else if (target == SaveTarget::Applications)
+ ShortcutUtils::createInstanceShortcutInApplications(args);
+ else
+ ShortcutUtils::createInstanceShortcutInOther(args);
+}
diff --git a/launcher/ui/dialogs/CreateShortcutDialog.h b/launcher/ui/dialogs/CreateShortcutDialog.h
new file mode 100644
index 000000000..cfedbf017
--- /dev/null
+++ b/launcher/ui/dialogs/CreateShortcutDialog.h
@@ -0,0 +1,62 @@
+/* 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 "BaseInstance.h"
+
+class BaseInstance;
+
+namespace Ui {
+class CreateShortcutDialog;
+}
+
+class CreateShortcutDialog : public QDialog {
+ Q_OBJECT
+
+ public:
+ explicit CreateShortcutDialog(InstancePtr instance, QWidget* parent = nullptr);
+ ~CreateShortcutDialog();
+
+ void createShortcut();
+
+ private slots:
+ // Icon, target and name
+ void on_iconButton_clicked();
+
+ // Override account
+ void on_overrideAccountCheckbox_stateChanged(int state);
+
+ // Override target (world, server)
+ void on_targetCheckbox_stateChanged(int state);
+ void on_worldTarget_toggled(bool checked);
+ void on_serverTarget_toggled(bool checked);
+ void on_worldSelectionBox_currentIndexChanged(int index);
+ void on_serverAddressBox_textChanged(const QString& text);
+
+ private:
+ // Data
+ Ui::CreateShortcutDialog* ui;
+ QString InstIconKey;
+ InstancePtr m_instance;
+ bool m_QuickJoinSupported = false;
+
+ // Index representations
+ enum class SaveTarget { Desktop, Applications, Other };
+
+ // Functions
+ void stateChanged();
+};
diff --git a/launcher/ui/dialogs/CreateShortcutDialog.ui b/launcher/ui/dialogs/CreateShortcutDialog.ui
new file mode 100644
index 000000000..9e2bdd747
--- /dev/null
+++ b/launcher/ui/dialogs/CreateShortcutDialog.ui
@@ -0,0 +1,250 @@
+
+
+ CreateShortcutDialog
+
+
+ Qt::WindowModality::ApplicationModal
+
+
+
+ 0
+ 0
+ 450
+ 370
+
+
+
+ Create Instance Shortcut
+
+
+ true
+
+
+ -
+
+
-
+
+
+
+ :/icons/instances/grass:/icons/instances/grass
+
+
+
+ 80
+ 80
+
+
+
+
+ -
+
+
-
+
+
+ Save To:
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ Name:
+
+
+
+ -
+
+
+ Name
+
+
+
+
+
+
+
+ -
+
+
+ Use a different account than the default specified.
+
+
+ Override the default account
+
+
+
+ -
+
+
+ false
+
+
+
+ 0
+ 0
+
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ -
+
+
+ Specify a world or server to automatically join on launch.
+
+
+ Select a target to join on launch
+
+
+
+ -
+
+
+ false
+
+
+
+ 0
+ 0
+
+
+
+
-
+
+
+ 0
+
+
-
+
+
+ World:
+
+
+ targetBtnGroup
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ 0
+
+
-
+
+
+ Server Address:
+
+
+ targetBtnGroup
+
+
+
+ -
+
+
+ false
+
+
+ Server Address:
+
+
+
+
+
+ -
+
+
+ Server Address
+
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+ QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok
+
+
+
+
+
+
+ iconButton
+
+
+
+
+ buttonBox
+ accepted()
+ CreateShortcutDialog
+ accept()
+
+
+ 20
+ 20
+
+
+ 20
+ 20
+
+
+
+
+ buttonBox
+ rejected()
+ CreateShortcutDialog
+ reject()
+
+
+ 20
+ 20
+
+
+ 20
+ 20
+
+
+
+
+
+
+
+
diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp
index 303df94a1..15420616e 100644
--- a/launcher/ui/dialogs/ExportPackDialog.cpp
+++ b/launcher/ui/dialogs/ExportPackDialog.cpp
@@ -17,7 +17,7 @@
*/
#include "ExportPackDialog.h"
-#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/mod/ResourceFolderModel.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlamePackExportTask.h"
#include "ui/dialogs/CustomMessageBox.h"
@@ -33,7 +33,7 @@
#include "MMCZip.h"
#include "modplatform/modrinth/ModrinthPackExportTask.h"
-ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider)
+ExportPackDialog::ExportPackDialog(MinecraftInstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider)
: QDialog(parent), m_instance(instance), m_ui(new Ui::ExportPackDialog), m_provider(provider)
{
Q_ASSERT(m_provider == ModPlatform::ResourceProvider::MODRINTH || m_provider == ModPlatform::ResourceProvider::FLAME);
@@ -44,12 +44,16 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla
m_ui->version->setText(instance->settings()->get("ExportVersion").toString());
m_ui->optionalFiles->setChecked(instance->settings()->get("ExportOptionalFiles").toBool());
+ connect(m_ui->recommendedMemoryCheckBox, &QCheckBox::toggled, m_ui->recommendedMemory, &QWidget::setEnabled);
+
if (m_provider == ModPlatform::ResourceProvider::MODRINTH) {
setWindowTitle(tr("Export Modrinth Pack"));
m_ui->authorLabel->hide();
m_ui->author->hide();
+ m_ui->recommendedMemoryWidget->hide();
+
m_ui->summary->setPlainText(instance->settings()->get("ExportSummary").toString());
} else {
setWindowTitle(tr("Export CurseForge Pack"));
@@ -57,6 +61,19 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla
m_ui->summaryLabel->hide();
m_ui->summary->hide();
+ const int recommendedRAM = instance->settings()->get("ExportRecommendedRAM").toInt();
+
+ if (recommendedRAM > 0) {
+ m_ui->recommendedMemoryCheckBox->setChecked(true);
+ m_ui->recommendedMemory->setValue(recommendedRAM);
+ } else {
+ m_ui->recommendedMemoryCheckBox->setChecked(false);
+
+ // recommend based on setting - limited to 12 GiB (CurseForge warns above this amount)
+ const int defaultRecommendation = qMin(m_instance->settings()->get("MaxMemAlloc").toInt(), 1024 * 12);
+ m_ui->recommendedMemory->setValue(defaultRecommendation);
+ }
+
m_ui->author->setText(instance->settings()->get("ExportAuthor").toString());
}
@@ -120,9 +137,15 @@ void ExportPackDialog::done(int result)
if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
settings->set("ExportSummary", m_ui->summary->toPlainText());
- else
+ else {
settings->set("ExportAuthor", m_ui->author->text());
+ if (m_ui->recommendedMemoryCheckBox->isChecked())
+ settings->set("ExportRecommendedRAM", m_ui->recommendedMemory->value());
+ else
+ settings->reset("ExportRecommendedRAM");
+ }
+
if (result == Accepted) {
const QString name = m_ui->name->text().isEmpty() ? m_instance->name() : m_ui->name->text();
const QString filename = FS::RemoveInvalidFilenameChars(name);
@@ -149,8 +172,18 @@ void ExportPackDialog::done(int result)
task = new ModrinthPackExportTask(name, m_ui->version->text(), m_ui->summary->toPlainText(), m_ui->optionalFiles->isChecked(),
m_instance, output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1));
} else {
- task = new FlamePackExportTask(name, m_ui->version->text(), m_ui->author->text(), m_ui->optionalFiles->isChecked(), m_instance,
- output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1));
+ FlamePackExportOptions options{};
+
+ options.name = name;
+ options.version = m_ui->version->text();
+ options.author = m_ui->author->text();
+ options.optionalFiles = m_ui->optionalFiles->isChecked();
+ options.instance = m_instance;
+ options.output = output;
+ options.filter = std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1);
+ options.recommendedRAM = m_ui->recommendedMemoryCheckBox->isChecked() ? m_ui->recommendedMemory->value() : 0;
+
+ task = new FlamePackExportTask(std::move(options));
}
connect(task, &Task::failed,
diff --git a/launcher/ui/dialogs/ExportPackDialog.h b/launcher/ui/dialogs/ExportPackDialog.h
index 092288d49..e93055d8d 100644
--- a/launcher/ui/dialogs/ExportPackDialog.h
+++ b/launcher/ui/dialogs/ExportPackDialog.h
@@ -22,6 +22,7 @@
#include "BaseInstance.h"
#include "FastFileIconProvider.h"
#include "FileIgnoreProxy.h"
+#include "minecraft/MinecraftInstance.h"
#include "modplatform/ModIndex.h"
namespace Ui {
@@ -32,7 +33,7 @@ class ExportPackDialog : public QDialog {
Q_OBJECT
public:
- explicit ExportPackDialog(InstancePtr instance,
+ explicit ExportPackDialog(MinecraftInstancePtr instance,
QWidget* parent = nullptr,
ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH);
~ExportPackDialog();
@@ -44,7 +45,7 @@ class ExportPackDialog : public QDialog {
QString ignoreFileName();
private:
- const InstancePtr m_instance;
+ const MinecraftInstancePtr m_instance;
Ui::ExportPackDialog* m_ui;
FileIgnoreProxy* m_proxy;
FastFileIconProvider m_icons;
diff --git a/launcher/ui/dialogs/ExportPackDialog.ui b/launcher/ui/dialogs/ExportPackDialog.ui
index a4a174212..bda8b8dd0 100644
--- a/launcher/ui/dialogs/ExportPackDialog.ui
+++ b/launcher/ui/dialogs/ExportPackDialog.ui
@@ -19,36 +19,56 @@
&Description
-
+
-
-
-
- &Name
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
-
- name
-
-
-
- -
-
-
- -
-
-
- &Version
-
-
- version
-
-
-
- -
-
-
- 1.0.0
-
-
+
-
+
+
+ &Name:
+
+
+ name
+
+
+
+ -
+
+
+ -
+
+
+ &Version:
+
+
+ version
+
+
+
+ -
+
+
+ 1.0.0
+
+
+
+ -
+
+
+ &Author:
+
+
+ author
+
+
+
+ -
+
+
+
-
@@ -62,24 +82,29 @@
-
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 100
+
+
+
+
+ 16777215
+ 100
+
+
true
- -
-
-
- &Author
-
-
- author
-
-
-
- -
-
-
@@ -88,7 +113,70 @@
&Options
-
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ &Recommended Memory:
+
+
+
+ -
+
+
+ false
+
+
+
+ 0
+ 0
+
+
+
+ MiB
+
+
+ 8
+
+
+ 32768
+
+
+ 128
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
-
@@ -138,10 +226,6 @@
- name
- version
- summary
- author
files
optionalFiles
diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp
index 83f46294d..14ec672e0 100644
--- a/launcher/ui/dialogs/MSALoginDialog.cpp
+++ b/launcher/ui/dialogs/MSALoginDialog.cpp
@@ -36,7 +36,6 @@
#include "MSALoginDialog.h"
#include "Application.h"
-#include "qr.h"
#include "ui_MSALoginDialog.h"
#include "DesktopServices.h"
@@ -44,10 +43,15 @@
#include
#include
+#include
+#include
#include
+#include
#include
#include
+#include "qrcodegen.hpp"
+
MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog)
{
ui->setupUi(this);
@@ -139,6 +143,33 @@ void MSALoginDialog::authorizeWithBrowser(const QUrl& url)
m_url = url;
}
+// https://stackoverflow.com/questions/21400254/how-to-draw-a-qr-code-with-qt-in-native-c-c
+void paintQR(QPainter& painter, const QSize sz, const QString& data, QColor fg)
+{
+ // NOTE: At this point you will use the API to get the encoding and format you want, instead of my hardcoded stuff:
+ qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data.toUtf8().constData(), qrcodegen::QrCode::Ecc::LOW);
+ const int s = qr.getSize() > 0 ? qr.getSize() : 1;
+ const double w = sz.width();
+ const double h = sz.height();
+ const double aspect = w / h;
+ const double size = ((aspect > 1.0) ? h : w);
+ const double scale = size / (s + 2);
+ // NOTE: For performance reasons my implementation only draws the foreground parts in supplied color.
+ // It expects background to be prepared already (in white or whatever is preferred).
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(fg);
+ for (int y = 0; y < s; y++) {
+ for (int x = 0; x < s; x++) {
+ const int color = qr.getModule(x, y); // 0 for white, 1 for black
+ if (0 != color) {
+ const double rx1 = (x + 1) * scale, ry1 = (y + 1) * scale;
+ QRectF r(rx1, ry1, scale, scale);
+ painter.drawRects(&r, 1);
+ }
+ }
+ }
+}
+
void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, [[maybe_unused]] int expiresIn)
{
ui->stackedWidget->setCurrentIndex(1);
diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp
index 3bc0bc2d9..8e661d37c 100644
--- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp
+++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp
@@ -92,6 +92,10 @@ SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct)
connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
SLOT(selectionChanged(QItemSelection, QItemSelection)));
connect(m_ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu);
+ connect(m_ui->elytraCB, &QCheckBox::stateChanged, this, [this]() {
+ m_skinPreview->setElytraVisible(m_ui->elytraCB->isChecked());
+ on_capeCombo_currentIndexChanged(0);
+ });
setupCapes();
@@ -159,10 +163,24 @@ void SkinManageDialog::on_fileBtn_clicked()
}
}
-QPixmap previewCape(QImage capeImage)
+QPixmap previewCape(QImage capeImage, bool elytra = false)
{
+ if (elytra) {
+ auto wing = capeImage.copy(34, 0, 12, 22);
+ QImage mirrored = wing.mirrored(true, false);
+
+ QImage combined(wing.width() * 2 - 2, wing.height(), capeImage.format());
+ combined.fill(Qt::transparent);
+
+ QPainter painter(&combined);
+ painter.drawImage(0, 0, wing);
+ painter.drawImage(wing.width() - 2, 0, mirrored);
+ painter.end();
+ return QPixmap::fromImage(combined.scaled(96, 176, Qt::IgnoreAspectRatio, Qt::FastTransformation));
+ }
return QPixmap::fromImage(capeImage.copy(1, 1, 10, 16).scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation));
}
+
void SkinManageDialog::setupCapes()
{
// FIXME: add a model for this, download/refresh the capes on demand
@@ -208,7 +226,7 @@ void SkinManageDialog::setupCapes()
}
}
if (!capeImage.isNull()) {
- m_ui->capeCombo->addItem(previewCape(capeImage), cape.alias, cape.id);
+ m_ui->capeCombo->addItem(previewCape(capeImage, m_ui->elytraCB->isChecked()), cape.alias, cape.id);
} else {
m_ui->capeCombo->addItem(cape.alias, cape.id);
}
@@ -222,7 +240,8 @@ void SkinManageDialog::on_capeCombo_currentIndexChanged(int index)
auto id = m_ui->capeCombo->currentData();
auto cape = m_capes.value(id.toString(), {});
if (!cape.isNull()) {
- m_ui->capeImage->setPixmap(previewCape(cape).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation));
+ m_ui->capeImage->setPixmap(
+ previewCape(cape, m_ui->elytraCB->isChecked()).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation));
} else {
m_ui->capeImage->clear();
}
@@ -319,14 +338,14 @@ bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev)
return QDialog::eventFilter(obj, ev);
}
-void SkinManageDialog::on_action_Rename_Skin_triggered(bool checked)
+void SkinManageDialog::on_action_Rename_Skin_triggered(bool)
{
if (!m_selectedSkinKey.isEmpty()) {
m_ui->listView->edit(m_ui->listView->currentIndex());
}
}
-void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked)
+void SkinManageDialog::on_action_Delete_Skin_triggered(bool)
{
if (m_selectedSkinKey.isEmpty())
return;
@@ -523,7 +542,7 @@ void SkinManageDialog::resizeEvent(QResizeEvent* event)
auto id = m_ui->capeCombo->currentData();
auto cape = m_capes.value(id.toString(), {});
if (!cape.isNull()) {
- m_ui->capeImage->setPixmap(previewCape(cape).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation));
+ m_ui->capeImage->setPixmap(previewCape(cape, m_ui->elytraCB->isChecked()).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation));
} else {
m_ui->capeImage->clear();
}
diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.ui b/launcher/ui/dialogs/skins/SkinManageDialog.ui
index 7e8b4bc46..aeb516854 100644
--- a/launcher/ui/dialogs/skins/SkinManageDialog.ui
+++ b/launcher/ui/dialogs/skins/SkinManageDialog.ui
@@ -59,6 +59,13 @@
Cape
+ -
+
+
+ Preview Elytra
+
+
+
-
diff --git a/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp b/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp
index b4ab8d4cc..f91fe2f1f 100644
--- a/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp
+++ b/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp
@@ -180,7 +180,8 @@ QList getCubeUVs(float u, float v, float width, float height, float d
}
namespace opengl {
-BoxGeometry::BoxGeometry(QVector3D size, QVector3D position) : m_indexBuf(QOpenGLBuffer::IndexBuffer), m_size(size), m_position(position)
+BoxGeometry::BoxGeometry(QVector3D size, QVector3D position)
+ : QOpenGLFunctions(), m_indexBuf(QOpenGLBuffer::IndexBuffer), m_size(size), m_position(position)
{
initializeOpenGLFunctions();
@@ -274,4 +275,9 @@ BoxGeometry* BoxGeometry::Plane()
return b;
}
+
+void BoxGeometry::scale(const QVector3D& vector)
+{
+ m_matrix.scale(vector);
+}
} // namespace opengl
\ No newline at end of file
diff --git a/launcher/ui/dialogs/skins/draw/BoxGeometry.h b/launcher/ui/dialogs/skins/draw/BoxGeometry.h
index 1a245bc14..fa1a4c622 100644
--- a/launcher/ui/dialogs/skins/draw/BoxGeometry.h
+++ b/launcher/ui/dialogs/skins/draw/BoxGeometry.h
@@ -36,6 +36,7 @@ class BoxGeometry : protected QOpenGLFunctions {
void initGeometry(float u, float v, float width, float height, float depth, float textureWidth = 64, float textureHeight = 64);
void rotate(float angle, const QVector3D& vector);
+ void scale(const QVector3D& vector);
private:
QOpenGLBuffer m_vertexBuf;
diff --git a/launcher/ui/dialogs/skins/draw/Scene.cpp b/launcher/ui/dialogs/skins/draw/Scene.cpp
index 45d0ba191..89a783622 100644
--- a/launcher/ui/dialogs/skins/draw/Scene.cpp
+++ b/launcher/ui/dialogs/skins/draw/Scene.cpp
@@ -18,9 +18,16 @@
*/
#include "ui/dialogs/skins/draw/Scene.h"
+
+#include
+#include
+#include
+#include
+
namespace opengl {
-Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : m_slim(slim), m_capeVisible(!cape.isNull())
+Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : QOpenGLFunctions(), m_slim(slim), m_capeVisible(!cape.isNull())
{
+ initializeOpenGLFunctions();
m_staticComponents = {
// head
new opengl::BoxGeometry(QVector3D(8, 8, 8), QVector3D(0, 4, 0), QPoint(0, 0), QVector3D(8, 8, 8)),
@@ -57,6 +64,19 @@ Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : m_slim(slim),
m_cape->rotate(10.8, QVector3D(1, 0, 0));
m_cape->rotate(180, QVector3D(0, 1, 0));
+ auto leftWing =
+ new opengl::BoxGeometry(QVector3D(12, 22, 4), QVector3D(0, -13, -2), QPoint(22, 0), QVector3D(10, 20, 2), QSize(64, 32));
+ leftWing->rotate(15, QVector3D(1, 0, 0));
+ leftWing->rotate(15, QVector3D(0, 0, 1));
+ leftWing->rotate(1, QVector3D(1, 0, 0));
+ auto rightWing =
+ new opengl::BoxGeometry(QVector3D(12, 22, 4), QVector3D(0, -13, -2), QPoint(22, 0), QVector3D(10, 20, 2), QSize(64, 32));
+ rightWing->scale(QVector3D(-1, 1, 1));
+ rightWing->rotate(15, QVector3D(1, 0, 0));
+ rightWing->rotate(15, QVector3D(0, 0, 1));
+ rightWing->rotate(1, QVector3D(1, 0, 0));
+ m_elytra << leftWing << rightWing;
+
// texture init
m_skinTexture = new QOpenGLTexture(skin.mirrored());
m_skinTexture->setMinificationFilter(QOpenGLTexture::Nearest);
@@ -68,7 +88,7 @@ Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : m_slim(slim),
}
Scene::~Scene()
{
- for (auto array : { m_staticComponents, m_normalArms, m_slimArms }) {
+ for (auto array : { m_staticComponents, m_normalArms, m_slimArms, m_elytra }) {
for (auto g : array) {
delete g;
}
@@ -95,7 +115,15 @@ void Scene::draw(QOpenGLShaderProgram* program)
if (m_capeVisible) {
m_capeTexture->bind();
program->setUniformValue("texture", 0);
- m_cape->draw(program);
+ if (!m_elytraVisible) {
+ m_cape->draw(program);
+ } else {
+ glDisable(GL_CULL_FACE);
+ for (auto e : m_elytra) {
+ e->draw(program);
+ }
+ glEnable(GL_CULL_FACE);
+ }
m_capeTexture->release();
}
}
@@ -131,4 +159,8 @@ void Scene::setCapeVisible(bool visible)
{
m_capeVisible = visible;
}
+void Scene::setElytraVisible(bool elytraVisible)
+{
+ m_elytraVisible = elytraVisible;
+}
} // namespace opengl
\ No newline at end of file
diff --git a/launcher/ui/dialogs/skins/draw/Scene.h b/launcher/ui/dialogs/skins/draw/Scene.h
index 3560d1d74..c9bba1f20 100644
--- a/launcher/ui/dialogs/skins/draw/Scene.h
+++ b/launcher/ui/dialogs/skins/draw/Scene.h
@@ -22,7 +22,7 @@
#include
namespace opengl {
-class Scene {
+class Scene : protected QOpenGLFunctions {
public:
Scene(const QImage& skin, bool slim, const QImage& cape);
virtual ~Scene();
@@ -32,15 +32,18 @@ class Scene {
void setCape(const QImage& cape);
void setMode(bool slim);
void setCapeVisible(bool visible);
+ void setElytraVisible(bool elytraVisible);
private:
QList m_staticComponents;
QList m_normalArms;
QList m_slimArms;
BoxGeometry* m_cape = nullptr;
+ QList m_elytra;
QOpenGLTexture* m_skinTexture = nullptr;
QOpenGLTexture* m_capeTexture = nullptr;
bool m_slim = false;
bool m_capeVisible = false;
+ bool m_elytraVisible = false;
};
} // namespace opengl
\ No newline at end of file
diff --git a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp
index e1e539050..f035e6b91 100644
--- a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp
+++ b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp
@@ -263,3 +263,8 @@ void SkinOpenGLWindow::wheelEvent(QWheelEvent* event)
m_distance = qMax(16.f, m_distance); // Clamp distance
update(); // Trigger a repaint
}
+void SkinOpenGLWindow::setElytraVisible(bool visible)
+{
+ if (m_scene)
+ m_scene->setElytraVisible(visible);
+}
diff --git a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h
index e2c32da0f..2a06c23e5 100644
--- a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h
+++ b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h
@@ -43,6 +43,7 @@ class SkinOpenGLWindow : public QOpenGLWindow, protected QOpenGLFunctions {
void updateScene(SkinModel* skin);
void updateCape(const QImage& cape);
+ void setElytraVisible(bool visible);
protected:
void mousePressEvent(QMouseEvent* e) override;
diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp
index b99d0c63e..6a44c9290 100644
--- a/launcher/ui/pages/global/JavaPage.cpp
+++ b/launcher/ui/pages/global/JavaPage.cpp
@@ -62,7 +62,7 @@
JavaPage::JavaPage(QWidget* parent) : QWidget(parent), ui(new Ui::JavaPage)
{
ui->setupUi(this);
-
+
if (BuildConfig.JAVA_DOWNLOADER_ENABLED) {
ui->managedJavaList->initialize(new JavaInstallList(this, true));
ui->managedJavaList->setResizeOn(2);
diff --git a/launcher/ui/pages/global/JavaPage.h b/launcher/ui/pages/global/JavaPage.h
index ea7724c1d..b30fa22e3 100644
--- a/launcher/ui/pages/global/JavaPage.h
+++ b/launcher/ui/pages/global/JavaPage.h
@@ -37,11 +37,11 @@
#include
#include
-#include "ui/widgets/JavaSettingsWidget.h"
#include
#include
#include "JavaCommon.h"
#include "ui/pages/BasePage.h"
+#include "ui/widgets/JavaSettingsWidget.h"
class SettingsObject;
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h
index fa1dce3dc..de173937b 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.h
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.h
@@ -35,17 +35,18 @@
#pragma once
+#include
#include "Application.h"
#include "BaseInstance.h"
#include "ui/pages/BasePage.h"
#include "ui/widgets/MinecraftSettingsWidget.h"
-#include
class InstanceSettingsPage : public MinecraftSettingsWidget, public BasePage {
Q_OBJECT
public:
- explicit InstanceSettingsPage(MinecraftInstancePtr instance, QWidget* parent = nullptr) : MinecraftSettingsWidget(std::move(instance), parent)
+ explicit InstanceSettingsPage(MinecraftInstancePtr instance, QWidget* parent = nullptr)
+ : MinecraftSettingsWidget(std::move(instance), parent)
{
connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::saveSettings);
connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings);
diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp
index 0fccd1d33..1738c9cde 100644
--- a/launcher/ui/pages/instance/ManagedPackPage.cpp
+++ b/launcher/ui/pages/instance/ManagedPackPage.cpp
@@ -347,13 +347,18 @@ void ManagedPackPage::onUpdateTaskCompleted(bool did_succeed) const
if (m_instance_window != nullptr)
m_instance_window->close();
- CustomMessageBox::selectable(nullptr, tr("Update Successful"), tr("The instance updated to pack version %1 successfully.").arg(m_inst->getManagedPackVersionName()), QMessageBox::Information)
- ->show();
+ CustomMessageBox::selectable(nullptr, tr("Update Successful"),
+ tr("The instance updated to pack version %1 successfully.").arg(m_inst->getManagedPackVersionName()),
+ QMessageBox::Information)
+ ->show();
} else {
- CustomMessageBox::selectable(nullptr, tr("Update Failed"), tr("The instance failed to update to pack version %1. Please check launcher logs for more information.").arg(m_inst->getManagedPackVersionName()), QMessageBox::Critical)
- ->show();
+ CustomMessageBox::selectable(
+ nullptr, tr("Update Failed"),
+ tr("The instance failed to update to pack version %1. Please check launcher logs for more information.")
+ .arg(m_inst->getManagedPackVersionName()),
+ QMessageBox::Critical)
+ ->show();
}
-
}
void ModrinthManagedPackPage::update()
diff --git a/launcher/ui/pages/instance/McClient.cpp b/launcher/ui/pages/instance/McClient.cpp
index 90813ac18..11b3a22d1 100644
--- a/launcher/ui/pages/instance/McClient.cpp
+++ b/launcher/ui/pages/instance/McClient.cpp
@@ -1,21 +1,22 @@
-#include
-#include
+#include
#include
#include
-#include
+#include
+#include
#include
-#include "McClient.h"
#include "Json.h"
+#include "McClient.h"
-// 7 first bits
+// 7 first bits
#define SEGMENT_BITS 0x7F
// last bit
#define CONTINUE_BIT 0x80
-McClient::McClient(QObject *parent, QString domain, QString ip, short port): QObject(parent), m_domain(domain), m_ip(ip), m_port(port) {}
+McClient::McClient(QObject* parent, QString domain, QString ip, short port) : QObject(parent), m_domain(domain), m_ip(ip), m_port(port) {}
-void McClient::getStatusData() {
+void McClient::getStatusData()
+{
qDebug() << "Connecting to socket..";
connect(&m_socket, &QTcpSocket::connected, this, [this]() {
@@ -25,28 +26,28 @@ void McClient::getStatusData() {
connect(&m_socket, &QTcpSocket::readyRead, this, &McClient::readRawResponse);
});
- connect(&m_socket, &QTcpSocket::errorOccurred, this, [this]() {
- emitFail("Socket disconnected: " + m_socket.errorString());
- });
+ connect(&m_socket, &QTcpSocket::errorOccurred, this, [this]() { emitFail("Socket disconnected: " + m_socket.errorString()); });
m_socket.connectToHost(m_ip, m_port);
}
-void McClient::sendRequest() {
+void McClient::sendRequest()
+{
QByteArray data;
- writeVarInt(data, 0x00); // packet ID
- writeVarInt(data, 763); // hardcoded protocol version (763 = 1.20.1)
- writeVarInt(data, m_domain.size()); // server address length
- writeString(data, m_domain.toStdString()); // server address
- writeFixedInt(data, m_port, 2); // server port
- writeVarInt(data, 0x01); // next state
- writePacketToSocket(data); // send handshake packet
+ writeVarInt(data, 0x00); // packet ID
+ writeVarInt(data, 763); // hardcoded protocol version (763 = 1.20.1)
+ writeVarInt(data, m_domain.size()); // server address length
+ writeString(data, m_domain.toStdString()); // server address
+ writeFixedInt(data, m_port, 2); // server port
+ writeVarInt(data, 0x01); // next state
+ writePacketToSocket(data); // send handshake packet
- writeVarInt(data, 0x00); // packet ID
- writePacketToSocket(data); // send status packet
+ writeVarInt(data, 0x00); // packet ID
+ writePacketToSocket(data); // send status packet
}
-void McClient::readRawResponse() {
+void McClient::readRawResponse()
+{
if (m_responseReadState == 2) {
return;
}
@@ -56,28 +57,27 @@ void McClient::readRawResponse() {
m_wantedRespLength = readVarInt(m_resp);
m_responseReadState = 1;
}
-
+
if (m_responseReadState == 1 && m_resp.size() >= m_wantedRespLength) {
if (m_resp.size() > m_wantedRespLength) {
- qDebug() << "Warning: Packet length doesn't match actual packet size (" << m_wantedRespLength << " expected vs " << m_resp.size() << " received)";
+ qDebug() << "Warning: Packet length doesn't match actual packet size (" << m_wantedRespLength << " expected vs "
+ << m_resp.size() << " received)";
}
parseResponse();
m_responseReadState = 2;
}
}
-void McClient::parseResponse() {
+void McClient::parseResponse()
+{
qDebug() << "Received response successfully";
int packetID = readVarInt(m_resp);
if (packetID != 0x00) {
- throw Exception(
- QString("Packet ID doesn't match expected value (0x00 vs 0x%1)")
- .arg(packetID, 0, 16)
- );
+ throw Exception(QString("Packet ID doesn't match expected value (0x00 vs 0x%1)").arg(packetID, 0, 16));
}
- Q_UNUSED(readVarInt(m_resp)); // json length
+ Q_UNUSED(readVarInt(m_resp)); // json length
// 'resp' should now be the JSON string
QJsonDocument doc = QJsonDocument::fromJson(m_resp);
@@ -85,8 +85,9 @@ void McClient::parseResponse() {
}
// From https://wiki.vg/Protocol#VarInt_and_VarLong
-void McClient::writeVarInt(QByteArray &data, int value) {
- while ((value & ~SEGMENT_BITS)) { // check if the value is too big to fit in 7 bits
+void McClient::writeVarInt(QByteArray& data, int value)
+{
+ while ((value & ~SEGMENT_BITS)) { // check if the value is too big to fit in 7 bits
// Write 7 bits
data.append((value & SEGMENT_BITS) | CONTINUE_BIT);
@@ -98,7 +99,8 @@ void McClient::writeVarInt(QByteArray &data, int value) {
}
// From https://wiki.vg/Protocol#VarInt_and_VarLong
-int McClient::readVarInt(QByteArray &data) {
+int McClient::readVarInt(QByteArray& data)
+{
int value = 0;
int position = 0;
char currentByte;
@@ -107,17 +109,20 @@ int McClient::readVarInt(QByteArray &data) {
currentByte = readByte(data);
value |= (currentByte & SEGMENT_BITS) << position;
- if ((currentByte & CONTINUE_BIT) == 0) break;
+ if ((currentByte & CONTINUE_BIT) == 0)
+ break;
position += 7;
}
- if (position >= 32) throw Exception("VarInt is too big");
+ if (position >= 32)
+ throw Exception("VarInt is too big");
return value;
}
-char McClient::readByte(QByteArray &data) {
+char McClient::readByte(QByteArray& data)
+{
if (data.isEmpty()) {
throw Exception("No more bytes to read");
}
@@ -128,17 +133,20 @@ char McClient::readByte(QByteArray &data) {
}
// write number with specified size in big endian format
-void McClient::writeFixedInt(QByteArray &data, int value, int size) {
+void McClient::writeFixedInt(QByteArray& data, int value, int size)
+{
for (int i = size - 1; i >= 0; i--) {
data.append((value >> (i * 8)) & 0xFF);
}
}
-void McClient::writeString(QByteArray &data, const std::string &value) {
+void McClient::writeString(QByteArray& data, const std::string& value)
+{
data.append(value.c_str());
}
-void McClient::writePacketToSocket(QByteArray &data) {
+void McClient::writePacketToSocket(QByteArray& data)
+{
// we prefix the packet with its length
QByteArray dataWithSize;
writeVarInt(dataWithSize, data.size());
@@ -151,14 +159,15 @@ void McClient::writePacketToSocket(QByteArray &data) {
data.clear();
}
-
-void McClient::emitFail(QString error) {
+void McClient::emitFail(QString error)
+{
qDebug() << "Minecraft server ping for status error:" << error;
emit failed(error);
emit finished();
}
-void McClient::emitSucceed(QJsonObject data) {
+void McClient::emitSucceed(QJsonObject data)
+{
emit succeeded(data);
emit finished();
}
diff --git a/launcher/ui/pages/instance/McClient.h b/launcher/ui/pages/instance/McClient.h
index 59834dfb7..832b70d40 100644
--- a/launcher/ui/pages/instance/McClient.h
+++ b/launcher/ui/pages/instance/McClient.h
@@ -1,8 +1,8 @@
-#include
-#include
+#include
#include
#include
-#include
+#include
+#include
#include
@@ -22,29 +22,30 @@ class McClient : public QObject {
unsigned m_wantedRespLength = 0;
QByteArray m_resp;
-public:
- explicit McClient(QObject *parent, QString domain, QString ip, short port);
+ public:
+ explicit McClient(QObject* parent, QString domain, QString ip, short port);
//! Read status data of the server, and calls the succeeded() signal with the parsed JSON data
void getStatusData();
-private:
+
+ private:
void sendRequest();
//! Accumulate data until we have a full response, then call parseResponse() once
void readRawResponse();
void parseResponse();
- void writeVarInt(QByteArray &data, int value);
- int readVarInt(QByteArray &data);
- char readByte(QByteArray &data);
+ void writeVarInt(QByteArray& data, int value);
+ int readVarInt(QByteArray& data);
+ char readByte(QByteArray& data);
//! write number with specified size in big endian format
- void writeFixedInt(QByteArray &data, int value, int size);
- void writeString(QByteArray &data, const std::string &value);
+ void writeFixedInt(QByteArray& data, int value, int size);
+ void writeString(QByteArray& data, const std::string& value);
- void writePacketToSocket(QByteArray &data);
+ void writePacketToSocket(QByteArray& data);
void emitFail(QString error);
void emitSucceed(QJsonObject data);
-signals:
+ signals:
void succeeded(QJsonObject data);
void failed(QString error);
void finished();
diff --git a/launcher/ui/pages/instance/McResolver.cpp b/launcher/ui/pages/instance/McResolver.cpp
index 48c2a72fd..2a769762c 100644
--- a/launcher/ui/pages/instance/McResolver.cpp
+++ b/launcher/ui/pages/instance/McResolver.cpp
@@ -1,23 +1,25 @@
-#include
-#include
#include
+#include
#include
+#include
#include "McResolver.h"
-McResolver::McResolver(QObject *parent, QString domain, int port): QObject(parent), m_constrDomain(domain), m_constrPort(port) {}
+McResolver::McResolver(QObject* parent, QString domain, int port) : QObject(parent), m_constrDomain(domain), m_constrPort(port) {}
-void McResolver::ping() {
+void McResolver::ping()
+{
pingWithDomainSRV(m_constrDomain, m_constrPort);
}
-void McResolver::pingWithDomainSRV(QString domain, int port) {
- QDnsLookup *lookup = new QDnsLookup(this);
+void McResolver::pingWithDomainSRV(QString domain, int port)
+{
+ QDnsLookup* lookup = new QDnsLookup(this);
lookup->setName(QString("_minecraft._tcp.%1").arg(domain));
lookup->setType(QDnsLookup::SRV);
connect(lookup, &QDnsLookup::finished, this, [this, domain, port]() {
- QDnsLookup *lookup = qobject_cast(sender());
+ QDnsLookup* lookup = qobject_cast(sender());
lookup->deleteLater();
@@ -43,8 +45,9 @@ void McResolver::pingWithDomainSRV(QString domain, int port) {
lookup->lookup();
}
-void McResolver::pingWithDomainA(QString domain, int port) {
- QHostInfo::lookupHost(domain, this, [this, port](const QHostInfo &hostInfo){
+void McResolver::pingWithDomainA(QString domain, int port)
+{
+ QHostInfo::lookupHost(domain, this, [this, port](const QHostInfo& hostInfo) {
if (hostInfo.error() != QHostInfo::NoError) {
emitFail("A record lookup failed");
return;
@@ -55,19 +58,21 @@ void McResolver::pingWithDomainA(QString domain, int port) {
emitFail("No A entries found for domain");
return;
}
-
+
const auto& firstRecord = records.at(0);
emitSucceed(firstRecord.toString(), port);
- });
+ });
}
-void McResolver::emitFail(QString error) {
+void McResolver::emitFail(QString error)
+{
qDebug() << "DNS resolver error:" << error;
emit failed(error);
emit finished();
}
-void McResolver::emitSucceed(QString ip, int port) {
+void McResolver::emitSucceed(QString ip, int port)
+{
emit succeeded(ip, port);
emit finished();
}
diff --git a/launcher/ui/pages/instance/McResolver.h b/launcher/ui/pages/instance/McResolver.h
index 06b4b7b38..3dfeddc6a 100644
--- a/launcher/ui/pages/instance/McResolver.h
+++ b/launcher/ui/pages/instance/McResolver.h
@@ -1,8 +1,8 @@
+#include
+#include
+#include
#include
#include
-#include
-#include
-#include
// resolve the IP and port of a Minecraft server
class McResolver : public QObject {
@@ -11,17 +11,17 @@ class McResolver : public QObject {
QString m_constrDomain;
int m_constrPort;
-public:
- explicit McResolver(QObject *parent, QString domain, int port);
+ public:
+ explicit McResolver(QObject* parent, QString domain, int port);
void ping();
-private:
+ private:
void pingWithDomainSRV(QString domain, int port);
void pingWithDomainA(QString domain, int port);
void emitFail(QString error);
void emitSucceed(QString ip, int port);
-signals:
+ signals:
void succeeded(QString ip, int port);
void failed(QString error);
void finished();
diff --git a/launcher/ui/pages/instance/ServerPingTask.cpp b/launcher/ui/pages/instance/ServerPingTask.cpp
index 3ec9308ca..b39f3d117 100644
--- a/launcher/ui/pages/instance/ServerPingTask.cpp
+++ b/launcher/ui/pages/instance/ServerPingTask.cpp
@@ -1,47 +1,41 @@
#include
-#include "ServerPingTask.h"
-#include "McResolver.h"
-#include "McClient.h"
#include
+#include "McClient.h"
+#include "McResolver.h"
+#include "ServerPingTask.h"
-unsigned getOnlinePlayers(QJsonObject data) {
+unsigned getOnlinePlayers(QJsonObject data)
+{
return Json::requireInteger(Json::requireObject(data, "players"), "online");
}
-void ServerPingTask::executeTask() {
+void ServerPingTask::executeTask()
+{
qDebug() << "Querying status of " << QString("%1:%2").arg(m_domain).arg(m_port);
// Resolve the actual IP and port for the server
- McResolver *resolver = new McResolver(nullptr, m_domain, m_port);
+ McResolver* resolver = new McResolver(nullptr, m_domain, m_port);
QObject::connect(resolver, &McResolver::succeeded, this, [this, resolver](QString ip, int port) {
qDebug() << "Resolved Address for" << m_domain << ": " << ip << ":" << port;
// Now that we have the IP and port, query the server
- McClient *client = new McClient(nullptr, m_domain, ip, port);
+ McClient* client = new McClient(nullptr, m_domain, ip, port);
QObject::connect(client, &McClient::succeeded, this, [this](QJsonObject data) {
m_outputOnlinePlayers = getOnlinePlayers(data);
qDebug() << "Online players: " << m_outputOnlinePlayers;
emitSucceeded();
});
- QObject::connect(client, &McClient::failed, this, [this](QString error) {
- emitFailed(error);
- });
+ QObject::connect(client, &McClient::failed, this, [this](QString error) { emitFailed(error); });
// Delete McClient object when done
- QObject::connect(client, &McClient::finished, this, [this, client]() {
- client->deleteLater();
- });
+ QObject::connect(client, &McClient::finished, this, [this, client]() { client->deleteLater(); });
client->getStatusData();
});
- QObject::connect(resolver, &McResolver::failed, this, [this](QString error) {
- emitFailed(error);
- });
+ QObject::connect(resolver, &McResolver::failed, this, [this](QString error) { emitFailed(error); });
// Delete McResolver object when done
- QObject::connect(resolver, &McResolver::finished, [resolver]() {
- resolver->deleteLater();
- });
+ QObject::connect(resolver, &McResolver::finished, [resolver]() { resolver->deleteLater(); });
resolver->ping();
}
\ No newline at end of file
diff --git a/launcher/ui/pages/instance/ServerPingTask.h b/launcher/ui/pages/instance/ServerPingTask.h
index 0956a4f63..6f03b92ad 100644
--- a/launcher/ui/pages/instance/ServerPingTask.h
+++ b/launcher/ui/pages/instance/ServerPingTask.h
@@ -5,18 +5,17 @@
#include
-
class ServerPingTask : public Task {
Q_OBJECT
- public:
+ public:
explicit ServerPingTask(QString domain, int port) : Task(), m_domain(domain), m_port(port) {}
~ServerPingTask() override = default;
int m_outputOnlinePlayers = -1;
- private:
+ private:
QString m_domain;
int m_port;
- protected:
+ protected:
virtual void executeTask() override;
};
diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp
index 8b4919015..803ba6d5c 100644
--- a/launcher/ui/pages/modplatform/ModPage.cpp
+++ b/launcher/ui/pages/modplatform/ModPage.cpp
@@ -61,7 +61,7 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePa
connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
}
-void ModPage::setFilterWidget(unique_qobject_ptr& widget)
+void ModPage::setFilterWidget(std::unique_ptr& widget)
{
if (m_filter_widget)
disconnect(m_filter_widget.get(), nullptr, nullptr, nullptr);
diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h
index 47fe21e0f..fb9f3f9d3 100644
--- a/launcher/ui/pages/modplatform/ModPage.h
+++ b/launcher/ui/pages/modplatform/ModPage.h
@@ -51,11 +51,11 @@ class ModPage : public ResourcePage {
void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr) override;
- virtual unique_qobject_ptr createFilterWidget() = 0;
+ virtual std::unique_ptr createFilterWidget() = 0;
[[nodiscard]] bool supportsFiltering() const override { return true; };
auto getFilter() const -> const std::shared_ptr { return m_filter; }
- void setFilterWidget(unique_qobject_ptr&);
+ void setFilterWidget(std::unique_ptr&);
protected:
ModPage(ModDownloadDialog* dialog, BaseInstance& instance);
@@ -67,7 +67,7 @@ class ModPage : public ResourcePage {
void triggerSearch() override;
protected:
- unique_qobject_ptr m_filter_widget;
+ std::unique_ptr m_filter_widget;
std::shared_ptr m_filter;
};
diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
index de6b3d633..bb91e5a64 100644
--- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
@@ -341,7 +341,7 @@ void FlamePage::setSearchTerm(QString term)
void FlamePage::createFilterWidget()
{
- auto widget = ModFilterWidget::create(nullptr, false, this);
+ auto widget = ModFilterWidget::create(nullptr, false);
m_filterWidget.swap(widget);
auto old = ui->splitter->replaceWidget(0, m_filterWidget.get());
// because we replaced the widget we also need to delete it
diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h
index 27c96d2f1..32b752bbe 100644
--- a/launcher/ui/pages/modplatform/flame/FlamePage.h
+++ b/launcher/ui/pages/modplatform/flame/FlamePage.h
@@ -100,6 +100,6 @@ class FlamePage : public QWidget, public ModpackProviderBasePage {
// Used to do instant searching with a delay to cache quick changes
QTimer m_search_timer;
- unique_qobject_ptr m_filterWidget;
+ std::unique_ptr m_filterWidget;
Task::Ptr m_categoriesTask;
};
diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp
index 609d77608..32175a356 100644
--- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp
@@ -246,9 +246,9 @@ auto FlameDataPackPage::shouldDisplay() const -> bool
return true;
}
-unique_qobject_ptr FlameModPage::createFilterWidget()
+std::unique_ptr FlameModPage::createFilterWidget()
{
- return ModFilterWidget::create(&static_cast(m_baseInstance), false, this);
+ return ModFilterWidget::create(&static_cast(m_baseInstance), false);
}
void FlameModPage::prepareProviderCategories()
diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h
index 306cdb4f3..309e1e019 100644
--- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h
+++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h
@@ -97,7 +97,7 @@ class FlameModPage : public ModPage {
[[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; }
void openUrl(const QUrl& url) override;
- unique_qobject_ptr createFilterWidget() override;
+ std::unique_ptr createFilterWidget() override;
protected:
virtual void prepareProviderCategories() override;
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
index 7d70abec4..701bb9f72 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp
@@ -391,7 +391,7 @@ QString ModrinthPage::getSerachTerm() const
void ModrinthPage::createFilterWidget()
{
- auto widget = ModFilterWidget::create(nullptr, true, this);
+ auto widget = ModFilterWidget::create(nullptr, true);
m_filterWidget.swap(widget);
auto old = ui->splitter->replaceWidget(0, m_filterWidget.get());
// because we replaced the widget we also need to delete it
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
index 7f504cdbd..d22a72e4e 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h
@@ -103,6 +103,6 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage {
// Used to do instant searching with a delay to cache quick changes
QTimer m_search_timer;
- unique_qobject_ptr m_filterWidget;
+ std::unique_ptr m_filterWidget;
Task::Ptr m_categoriesTask;
};
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
index 99f0239da..f75323a28 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
@@ -165,19 +165,19 @@ auto ModrinthDataPackPage::shouldDisplay() const -> bool
return true;
}
-unique_qobject_ptr ModrinthModPage::createFilterWidget()
+std::unique_ptr ModrinthModPage::createFilterWidget()
{
- return ModFilterWidget::create(&static_cast(m_baseInstance), true, this);
+ return ModFilterWidget::create(&static_cast(m_baseInstance), true);
}
void ModrinthModPage::prepareProviderCategories()
{
auto response = std::make_shared();
- auto task = ModrinthAPI::getModCategories(response);
- QObject::connect(task.get(), &Task::succeeded, [this, response]() {
+ m_categoriesTask = ModrinthAPI::getModCategories(response);
+ QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() {
auto categories = ModrinthAPI::loadModCategories(response);
m_filter_widget->setCategories(categories);
});
- task->start();
+ m_categoriesTask->start();
};
} // namespace ResourceDownload
diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h
index d38a692a8..a4c7344b5 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h
@@ -95,10 +95,11 @@ class ModrinthModPage : public ModPage {
[[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; }
- unique_qobject_ptr createFilterWidget() override;
+ std::unique_ptr createFilterWidget() override;
protected:
virtual void prepareProviderCategories() override;
+ Task::Ptr m_categoriesTask;
};
class ModrinthResourcePackPage : public ResourcePackResourcePage {
diff --git a/launcher/ui/themes/HintOverrideProxyStyle.cpp b/launcher/ui/themes/HintOverrideProxyStyle.cpp
index f31969fce..f5b8232a8 100644
--- a/launcher/ui/themes/HintOverrideProxyStyle.cpp
+++ b/launcher/ui/themes/HintOverrideProxyStyle.cpp
@@ -18,7 +18,8 @@
#include "HintOverrideProxyStyle.h"
-HintOverrideProxyStyle::HintOverrideProxyStyle(QStyle* style) : QProxyStyle(style) {
+HintOverrideProxyStyle::HintOverrideProxyStyle(QStyle* style) : QProxyStyle(style)
+{
setObjectName(style->objectName());
}
diff --git a/launcher/ui/widgets/CheckComboBox.cpp b/launcher/ui/widgets/CheckComboBox.cpp
index 02b629162..57d98ea7f 100644
--- a/launcher/ui/widgets/CheckComboBox.cpp
+++ b/launcher/ui/widgets/CheckComboBox.cpp
@@ -178,7 +178,7 @@ QStringList CheckComboBox::checkedItems() const
void CheckComboBox::setCheckedItems(const QStringList& items)
{
- foreach (auto text, items) {
+ for (auto text : items) {
auto index = findText(text);
setItemCheckState(index, index != -1 ? Qt::Checked : Qt::Unchecked);
}
diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp
index 2802f0746..8d6505655 100644
--- a/launcher/ui/widgets/InfoFrame.cpp
+++ b/launcher/ui/widgets/InfoFrame.cpp
@@ -293,7 +293,7 @@ void InfoFrame::setDescription(QString text)
QChar rem('\n');
QString finaltext;
finaltext.reserve(intermediatetext.size());
- foreach (const QChar& c, intermediatetext) {
+ for (const QChar& c : intermediatetext) {
if (c == rem && prev) {
continue;
}
@@ -347,7 +347,7 @@ void InfoFrame::setLicense(QString text)
QChar rem('\n');
QString finaltext;
finaltext.reserve(intermediatetext.size());
- foreach (const QChar& c, intermediatetext) {
+ for (const QChar& c : intermediatetext) {
if (c == rem && prev) {
continue;
}
diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp
index 03522bc19..da41b990a 100644
--- a/launcher/ui/widgets/ModFilterWidget.cpp
+++ b/launcher/ui/widgets/ModFilterWidget.cpp
@@ -49,9 +49,9 @@
#include "Application.h"
#include "minecraft/PackProfile.h"
-unique_qobject_ptr ModFilterWidget::create(MinecraftInstance* instance, bool extended, QWidget* parent)
+std::unique_ptr ModFilterWidget::create(MinecraftInstance* instance, bool extended)
{
- return unique_qobject_ptr(new ModFilterWidget(instance, extended, parent));
+ return std::unique_ptr(new ModFilterWidget(instance, extended));
}
class VersionBasicModel : public QIdentityProxyModel {
@@ -107,8 +107,8 @@ class AllVersionProxyModel : public QSortFilterProxyModel {
}
};
-ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWidget* parent)
- : QTabWidget(parent), ui(new Ui::ModFilterWidget), m_instance(instance), m_filter(new Filter())
+ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended)
+ : QTabWidget(), ui(new Ui::ModFilterWidget), m_instance(instance), m_filter(new Filter())
{
ui->setupUi(this);
diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h
index 41a2f1bbd..88f2593dd 100644
--- a/launcher/ui/widgets/ModFilterWidget.h
+++ b/launcher/ui/widgets/ModFilterWidget.h
@@ -83,7 +83,7 @@ class ModFilterWidget : public QTabWidget {
}
};
- static unique_qobject_ptr create(MinecraftInstance* instance, bool extended, QWidget* parent = nullptr);
+ static std::unique_ptr create(MinecraftInstance* instance, bool extended);
virtual ~ModFilterWidget();
auto getFilter() -> std::shared_ptr;
@@ -96,7 +96,7 @@ class ModFilterWidget : public QTabWidget {
void setCategories(const QList&);
private:
- ModFilterWidget(MinecraftInstance* instance, bool extendedSupport, QWidget* parent = nullptr);
+ ModFilterWidget(MinecraftInstance* instance, bool extendedSupport);
void loadVersionList();
void prepareBasicFilter();
diff --git a/launcher/updater/MacSparkleUpdater.mm b/launcher/updater/MacSparkleUpdater.mm
index b2b631593..07862c9a3 100644
--- a/launcher/updater/MacSparkleUpdater.mm
+++ b/launcher/updater/MacSparkleUpdater.mm
@@ -166,7 +166,7 @@ void MacSparkleUpdater::setAllowedChannels(const QSet& channels) {
QString channelsConfig = "";
// Convert QSet -> NSSet
NSMutableSet* nsChannels = [NSMutableSet setWithCapacity:channels.count()];
- foreach (const QString channel, channels) {
+ for (const QString channel : channels) {
[nsChannels addObject:channel.toNSString()];
channelsConfig += channel + " ";
}
diff --git a/libraries/README.md b/libraries/README.md
index 5f7b685e5..be41e549f 100644
--- a/libraries/README.md
+++ b/libraries/README.md
@@ -99,7 +99,7 @@ Canonical implementation of the murmur2 hash, taken from [SMHasher](https://gith
Public domain (the author disclaimed the copyright).
-## qt-qrcodegenerator
+## QR-Code-generator
A simple library for generating QR codes
diff --git a/libraries/qrcodegenerator b/libraries/qrcodegenerator
new file mode 160000
index 000000000..2c9044de6
--- /dev/null
+++ b/libraries/qrcodegenerator
@@ -0,0 +1 @@
+Subproject commit 2c9044de6b049ca25cb3cd1649ed7e27aa055138
diff --git a/libraries/qt-qrcodegenerator/CMakeLists.txt b/libraries/qt-qrcodegenerator/CMakeLists.txt
deleted file mode 100644
index e18da0e71..000000000
--- a/libraries/qt-qrcodegenerator/CMakeLists.txt
+++ /dev/null
@@ -1,32 +0,0 @@
-cmake_minimum_required(VERSION 3.6)
-
-project(qrcode)
-
-set(CMAKE_AUTOMOC ON)
-set(CMAKE_INCLUDE_CURRENT_DIR ON)
-
-set(CMAKE_CXX_STANDARD_REQUIRED true)
-set(CMAKE_C_STANDARD_REQUIRED true)
-set(CMAKE_CXX_STANDARD 11)
-set(CMAKE_C_STANDARD 11)
-
-
-if(QT_VERSION_MAJOR EQUAL 5)
- find_package(Qt5 COMPONENTS Core Gui REQUIRED)
-elseif(Launcher_QT_VERSION_MAJOR EQUAL 6)
- find_package(Qt6 COMPONENTS Core Gui Core5Compat REQUIRED)
- list(APPEND systeminfo_LIBS Qt${QT_VERSION_MAJOR}::Core5Compat)
-endif()
-
-add_library(qrcode STATIC qr.h qr.cpp QR-Code-generator/cpp/qrcodegen.cpp QR-Code-generator/cpp/qrcodegen.hpp )
-
-target_link_libraries(qrcode Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui ${systeminfo_LIBS})
-
-
-# needed for statically linked qrcode in shared libs on x86_64
-set_target_properties(qrcode
- PROPERTIES POSITION_INDEPENDENT_CODE TRUE
-)
-
-target_include_directories(qrcode PUBLIC ./ PRIVATE QR-Code-generator/cpp/)
-
diff --git a/libraries/qt-qrcodegenerator/QR-Code-generator b/libraries/qt-qrcodegenerator/QR-Code-generator
deleted file mode 160000
index f40366c40..000000000
--- a/libraries/qt-qrcodegenerator/QR-Code-generator
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit f40366c40d8d1956081f7ec643d240c02a81df52
diff --git a/libraries/qt-qrcodegenerator/qr.cpp b/libraries/qt-qrcodegenerator/qr.cpp
deleted file mode 100644
index 69bfb6da5..000000000
--- a/libraries/qt-qrcodegenerator/qr.cpp
+++ /dev/null
@@ -1,29 +0,0 @@
-
-#include "qr.h"
-#include "qrcodegen.hpp"
-
-void paintQR(QPainter& painter, const QSize sz, const QString& data, QColor fg)
-{
- // NOTE: At this point you will use the API to get the encoding and format you want, instead of my hardcoded stuff:
- qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data.toUtf8().constData(), qrcodegen::QrCode::Ecc::LOW);
- const int s = qr.getSize() > 0 ? qr.getSize() : 1;
- const double w = sz.width();
- const double h = sz.height();
- const double aspect = w / h;
- const double size = ((aspect > 1.0) ? h : w);
- const double scale = size / (s + 2);
- // NOTE: For performance reasons my implementation only draws the foreground parts in supplied color.
- // It expects background to be prepared already (in white or whatever is preferred).
- painter.setPen(Qt::NoPen);
- painter.setBrush(fg);
- for (int y = 0; y < s; y++) {
- for (int x = 0; x < s; x++) {
- const int color = qr.getModule(x, y); // 0 for white, 1 for black
- if (0 != color) {
- const double rx1 = (x + 1) * scale, ry1 = (y + 1) * scale;
- QRectF r(rx1, ry1, scale, scale);
- painter.drawRects(&r, 1);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/libraries/qt-qrcodegenerator/qr.h b/libraries/qt-qrcodegenerator/qr.h
deleted file mode 100644
index 290d49001..000000000
--- a/libraries/qt-qrcodegenerator/qr.h
+++ /dev/null
@@ -1,8 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-
-// https://stackoverflow.com/questions/21400254/how-to-draw-a-qr-code-with-qt-in-native-c-c
-void paintQR(QPainter& painter, const QSize sz, const QString& data, QColor fg);
diff --git a/nix/unwrapped.nix b/nix/unwrapped.nix
index b5b02b101..d9144410f 100644
--- a/nix/unwrapped.nix
+++ b/nix/unwrapped.nix
@@ -9,7 +9,7 @@
jdk17,
kdePackages,
libnbtplusplus,
- qt-qrcodegenerator,
+ qrcodegenerator,
ninja,
self,
stripJavaArchivesHook,
@@ -64,8 +64,8 @@ stdenv.mkDerivation {
rm -rf source/libraries/libnbtplusplus
ln -s ${libnbtplusplus} source/libraries/libnbtplusplus
- rm -rf source/libraries/qt-qrcodegenerator/QR-Code-generator
- ln -s ${qt-qrcodegenerator} source/libraries/qt-qrcodegenerator/QR-Code-generator
+ rm -rf source/libraries/qrcodegenerator
+ ln -s ${qrcodegenerator} source/libraries/qrcodegenerator
'';
nativeBuildInputs = [