diff --git a/.clang-tidy b/.clang-tidy
index 436dcf244..ef5166da4 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -1,5 +1,23 @@
Checks:
- modernize-use-using
- readability-avoid-const-params-in-decls
+ - misc-unused-parameters,
+ - readability-identifier-naming
-SystemHeaders: false
+# ^ Without unused-parameters the readability-identifier-naming check doesn't cause any warnings.
+
+CheckOptions:
+ - { key: readability-identifier-naming.ClassCase, value: PascalCase }
+ - { key: readability-identifier-naming.EnumCase, value: PascalCase }
+ - { key: readability-identifier-naming.FunctionCase, value: camelCase }
+ - { key: readability-identifier-naming.GlobalVariableCase, value: camelCase }
+ - { key: readability-identifier-naming.GlobalFunctionCase, value: camelCase }
+ - { key: readability-identifier-naming.GlobalConstantCase, value: SCREAMING_SNAKE_CASE }
+ - { key: readability-identifier-naming.MacroDefinitionCase, value: SCREAMING_SNAKE_CASE }
+ - { key: readability-identifier-naming.ClassMemberCase, value: camelCase }
+ - { key: readability-identifier-naming.PrivateMemberPrefix, value: m_ }
+ - { key: readability-identifier-naming.ProtectedMemberPrefix, value: m_ }
+ - { key: readability-identifier-naming.PrivateStaticMemberPrefix, value: s_ }
+ - { key: readability-identifier-naming.ProtectedStaticMemberPrefix, value: s_ }
+ - { key: readability-identifier-naming.PublicStaticConstantCase, value: SCREAMING_SNAKE_CASE }
+ - { key: readability-identifier-naming.EnumConstantCase, value: SCREAMING_SNAKE_CASE }
\ No newline at end of file
diff --git a/.envrc b/.envrc
index 190b5b2b3..1d11c5354 100644
--- a/.envrc
+++ b/.envrc
@@ -1,2 +1,2 @@
-use flake
+use nix
watch_file nix/*.nix
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/get-merge-commit/action.yml b/.github/actions/get-merge-commit/action.yml
new file mode 100644
index 000000000..534d138e1
--- /dev/null
+++ b/.github/actions/get-merge-commit/action.yml
@@ -0,0 +1,103 @@
+# This file incorporates work covered by the following copyright and
+# permission notice
+#
+# Copyright (c) 2003-2025 Eelco Dolstra and the Nixpkgs/NixOS contributors
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+name: Get merge commit
+description: Get a merge commit of a given pull request
+
+inputs:
+ repository:
+ description: Repository containing the pull request
+ required: false
+ pull-request-id:
+ description: ID of a pull request
+ required: true
+
+outputs:
+ merge-commit-sha:
+ description: Git SHA of a merge commit
+ value: ${{ steps.query.outputs.merge-commit-sha }}
+
+runs:
+ using: composite
+
+ steps:
+ - name: Wait for GitHub to report merge commit
+ id: query
+ shell: bash
+ env:
+ GITHUB_REPO: ${{ inputs.repository || github.repository }}
+ PR_ID: ${{ inputs.pull-request-id }}
+ # https://github.com/NixOS/nixpkgs/blob/8f77f3600f1ee775b85dc2c72fd842768e486ec9/ci/get-merge-commit.sh
+ run: |
+ set -euo pipefail
+
+ log() {
+ echo "$@" >&2
+ }
+
+ # Retry the API query this many times
+ retryCount=5
+ # Start with 5 seconds, but double every retry
+ retryInterval=5
+
+ while true; do
+ log "Checking whether the pull request can be merged"
+ prInfo=$(gh api \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "/repos/$GITHUB_REPO/pulls/$PR_ID")
+
+ # Non-open PRs won't have their mergeability computed no matter what
+ state=$(jq -r .state <<<"$prInfo")
+ if [[ "$state" != open ]]; then
+ log "PR is not open anymore"
+ exit 1
+ fi
+
+ mergeable=$(jq -r .mergeable <<<"$prInfo")
+ if [[ "$mergeable" == "null" ]]; then
+ if ((retryCount == 0)); then
+ log "Not retrying anymore. It's likely that GitHub is having internal issues: check https://www.githubstatus.com/"
+ exit 3
+ else
+ ((retryCount -= 1)) || true
+
+ # null indicates that GitHub is still computing whether it's mergeable
+ # Wait a couple seconds before trying again
+ log "GitHub is still computing whether this PR can be merged, waiting $retryInterval seconds before trying again ($retryCount retries left)"
+ sleep "$retryInterval"
+
+ ((retryInterval *= 2)) || true
+ fi
+ else
+ break
+ fi
+ done
+
+ if [[ "$mergeable" == "true" ]]; then
+ echo "merge-commit-sha=$(jq -r .merge_commit_sha <<<"$prInfo")" >> "$GITHUB_OUTPUT"
+ else
+ echo "# 🚨 The PR has a merge conflict!" >> "$GITHUB_STEP_SUMMARY"
+ exit 2
+ fi
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..78717ddf4
--- /dev/null
+++ b/.github/actions/setup-dependencies/windows/action.yml
@@ -0,0 +1,66 @@
+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-64)
+ 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
+ quazip-qt6:p
+
+ - 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/backport.yml b/.github/workflows/backport.yml
index 4146cddf4..c46f8e192 100644
--- a/.github/workflows/backport.yml
+++ b/.github/workflows/backport.yml
@@ -25,7 +25,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs
- uses: korthout/backport-action@v3.1.0
+ uses: korthout/backport-action@v3.2.0
with:
# Config README: https://github.com/korthout/backport-action#backport-action
pull_description: |-
diff --git a/.github/workflows/blocked-prs.yml b/.github/workflows/blocked-prs.yml
new file mode 100644
index 000000000..ecbaf755d
--- /dev/null
+++ b/.github/workflows/blocked-prs.yml
@@ -0,0 +1,255 @@
+name: Blocked/Stacked Pull Requests Automation
+
+on:
+ pull_request_target:
+ types:
+ - opened
+ - reopened
+ - edited
+ - synchronize
+ workflow_dispatch:
+ inputs:
+ pr_id:
+ description: Local Pull Request number to work on
+ required: true
+ type: number
+
+jobs:
+ blocked_status:
+ name: Check Blocked Status
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Generate token
+ id: generate-token
+ uses: actions/create-github-app-token@v2
+ with:
+ app-id: ${{ vars.PULL_REQUEST_APP_ID }}
+ private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }}
+
+ - name: Setup From Dispatch Event
+ if: github.event_name == 'workflow_dispatch'
+ id: dispatch_event_setup
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ PR_NUMBER: ${{ inputs.pr_id }}
+ run: |
+ # setup env for the rest of the workflow
+ OWNER=$(dirname "${{ github.repository }}")
+ REPO=$(basename "${{ github.repository }}")
+ PR_JSON=$(
+ gh api \
+ -H "Accept: application/vnd.github.raw+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "/repos/$OWNER/$REPO/pulls/$PR_NUMBER"
+ )
+ echo "PR_JSON=$PR_JSON" >> "$GITHUB_ENV"
+
+ - name: Setup Environment
+ id: env_setup
+ env:
+ EVENT_PR_JSON: ${{ toJSON(github.event.pull_request) }}
+ run: |
+ # setup env for the rest of the workflow
+ PR_JSON=${PR_JSON:-"$EVENT_PR_JSON"}
+ {
+ echo "REPO=$(jq -r '.base.repo.name' <<< "$PR_JSON")"
+ echo "OWNER=$(jq -r '.base.repo.owner.login' <<< "$PR_JSON")"
+ echo "PR_NUMBER=$(jq -r '.number' <<< "$PR_JSON")"
+ echo "JOB_DATA=$(jq -c '
+ {
+ "repo": .base.repo.name,
+ "owner": .base.repo.owner.login,
+ "repoUrl": .base.repo.html_url,
+ "prNumber": .number,
+ "prHeadSha": .head.sha,
+ "prHeadLabel": .head.label,
+ "prBody": (.body // ""),
+ "prLabels": (reduce .labels[].name as $l ([]; . + [$l]))
+ }
+ ' <<< "$PR_JSON")"
+ } >> "$GITHUB_ENV"
+
+
+ - name: Find Blocked/Stacked PRs in body
+ id: pr_ids
+ run: |
+ prs=$(
+ jq -c '
+ .prBody as $body
+ | (
+ $body |
+ reduce (
+ . | scan("blocked (?:by|on):? #([0-9]+)")
+ | map({
+ "type": "Blocked on",
+ "number": ( . | tonumber )
+ })
+ ) as $i ([]; . + [$i[]])
+ ) as $bprs
+ | (
+ $body |
+ reduce (
+ . | scan("stacked on:? #([0-9]+)")
+ | map({
+ "type": "Stacked on",
+ "number": ( . | tonumber )
+ })
+ ) as $i ([]; . + [$i[]])
+ ) as $sprs
+ | ($bprs + $sprs) as $prs
+ | {
+ "blocking": $prs,
+ "numBlocking": ( $prs | length),
+ }
+ ' <<< "$JOB_DATA"
+ )
+ echo "prs=$prs" >> "$GITHUB_OUTPUT"
+
+ - name: Collect Blocked PR Data
+ id: blocking_data
+ if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ BLOCKING_PRS: ${{ steps.pr_ids.outputs.prs }}
+ run: |
+ blocked_pr_data=$(
+ while read -r pr_data ; do
+ gh api \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "/repos/$OWNER/$REPO/pulls/$(jq -r '.number' <<< "$pr_data")" \
+ | jq -c --arg type "$(jq -r '.type' <<< "$pr_data")" \
+ '
+ . | {
+ "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,
+ "baseRepoOwner": .head.repo.owner.login,
+ "baseRepoUrl": .head.repo.html_url,
+ "baseSha": .head.sha,
+ "baseRefName": .head.ref,
+ }
+ '
+ done < <(jq -c '.blocking[]' <<< "$BLOCKING_PRS") | jq -c -s
+ )
+ {
+ echo "data=$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 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
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ run: |
+ gh -R ${{ github.repository }} issue edit --add-label 'blocked' "$PR_NUMBER"
+
+ - name: Remove 'blocked' Label if All Dependencies Are Merged
+ id: unlabel_blocked
+ if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 && fromJSON(steps.blocking_data.outputs.all_merged)
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ run: |
+ gh -R ${{ github.repository }} issue edit --remove-label 'blocked' "$PR_NUMBER"
+
+ - name: Apply 'blocking' Label to Unmerged Dependencies
+ id: label_blocking
+ if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ BLOCKING_ISSUES: ${{ steps.blocking_data.outputs.current_blocking }}
+ run: |
+ while read -r pr ; do
+ gh -R ${{ github.repository }} issue edit --add-label 'blocking' "$pr" || true
+ done < <(jq -c '.[]' <<< "$BLOCKING_ISSUES")
+
+ - name: Apply Blocking PR Status Check
+ id: blocked_check
+ if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ BLOCKING_DATA: ${{ steps.blocking_data.outputs.data }}
+ run: |
+ pr_head_sha=$(jq -r '.prHeadSha' <<< "$JOB_DATA")
+ # create commit Status, overwrites previous identical context
+ while read -r pr_data ; do
+ DESC=$(
+ 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 (.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")"
+ done < <(jq -c '.[]' <<< "$BLOCKING_DATA")
+
+ - name: Context Comment
+ id: generate-comment
+ if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
+ continue-on-error: true
+ env:
+ BLOCKING_DATA: ${{ steps.blocking_data.outputs.data }}
+ run: |
+ COMMENT_PATH="$(pwd)/temp_comment_file.txt"
+ echo '
PR Dependencies :pushpin:
' > "$COMMENT_PATH"
+ echo >> "$COMMENT_PATH"
+ pr_head_label=$(jq -r '.prHeadLabel' <<< "$JOB_DATA")
+ while read -r pr_data ; do
+ base_pr=$(jq -r '.number' <<< "$pr_data")
+ base_ref_name=$(jq -r '.baseRefName' <<< "$pr_data")
+ 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 .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")
+
+ {
+ echo 'body<> "$GITHUB_OUTPUT"
+
+ - name: 💬 PR Comment
+ if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ COMMENT_BODY: ${{ steps.generate-comment.outputs.body }}
+ run: |
+ gh -R ${{ github.repository }} issue comment "$PR_NUMBER" \
+ --body "$COMMENT_BODY" \
+ --create-if-none \
+ --edit-last
+
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a1ed0a25a..ac33aba27 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,699 +1,195 @@
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"
+ 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"
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
- CACHIX_AUTH_TOKEN:
- description: Private token for authenticating against Cachix cache
- 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:
- strategy:
- fail-fast: false
- matrix:
- include:
- - os: ubuntu-20.04
- qt_ver: 5
- qt_host: linux
- qt_arch: ""
- qt_version: "5.12.8"
- qt_modules: "qtnetworkauth"
-
- - os: ubuntu-20.04
- qt_ver: 6
- qt_host: linux
- qt_arch: ""
- qt_version: "6.2.4"
- qt_modules: "qt5compat qtimageformats qtnetworkauth"
-
- - os: windows-2022
- name: "Windows-MinGW-w64"
- msystem: clang64
- vcvars_arch: "amd64_x86"
-
- - os: windows-2022
- name: "Windows-MSVC"
- msystem: ""
- architecture: "x64"
- vcvars_arch: "amd64"
- qt_ver: 6
- qt_host: windows
- qt_arch: ""
- qt_version: "6.7.3"
- qt_modules: "qt5compat qtimageformats qtnetworkauth"
- nscurl_tag: "v24.9.26.122"
- nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
-
- - os: windows-2022
- name: "Windows-MSVC-arm64"
- msystem: ""
- architecture: "arm64"
- vcvars_arch: "amd64_arm64"
- qt_ver: 6
- qt_host: windows
- qt_arch: "win64_msvc2019_arm64"
- qt_version: "6.7.3"
- qt_modules: "qt5compat qtimageformats qtnetworkauth"
- nscurl_tag: "v24.9.26.122"
- nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
-
- - os: macos-14
- name: macOS
- macosx_deployment_target: 11.0
- qt_ver: 6
- qt_host: mac
- qt_arch: ""
- qt_version: "6.7.3"
- qt_modules: "qt5compat qtimageformats qtnetworkauth"
-
- - os: macos-14
- name: macOS-Legacy
- macosx_deployment_target: 10.13
- qt_ver: 5
- qt_host: mac
- qt_version: "5.15.2"
- qt_modules: "qtnetworkauth"
-
- runs-on: ${{ matrix.os }}
-
- 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
-
- steps:
- ##
- # PREPARE
- ##
- - name: Checkout
- uses: actions/checkout@v4
- with:
- submodules: "true"
-
- - name: "Setup MSYS2"
- if: runner.os == 'Windows' && matrix.msystem != ''
- uses: msys2/setup-msys2@v2
- with:
- 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.14
- with:
- key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
-
- - name: Retrieve ccache cache (Windows MinGW-w64)
- if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
- uses: actions/cache@v4.1.1
- 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: Use ccache on Debug builds only
- if: inputs.build_type == 'Debug'
- shell: bash
- run: |
- echo "CCACHE_VAR=ccache" >> $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
-
- - 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@v3
- with:
- aqtversion: "==3.1.*"
- py7zrversion: ">=0.20.2"
- version: ${{ matrix.qt_version }}
- host: "windows"
- target: "desktop"
- 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@v3
- 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' && matrix.qt_ver != 5
- run: |
- wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
- wget "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage"
- wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage"
-
- wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage"
-
- 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 }}\msvc2019_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' && matrix.qt_ver == 6
- run: |
- cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -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 (macOS-Legacy)
- if: runner.os == 'macOS' && matrix.qt_ver == 5
- run: |
- cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_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 }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -DCMAKE_OSX_ARCHITECTURES="x86_64" -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 == ''
- 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 }}
- # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix)
- if ("${{ env.CCACHE_VAR }}")
- {
- 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: 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
-
- ##
- # BUILD
- ##
-
- - name: Build
- if: runner.os != 'Windows'
- run: |
- cmake --build ${{ env.BUILD_DIR }}
-
- - name: Build (Windows MinGW-w64)
- if: runner.os == 'Windows' && matrix.msystem != ''
- shell: msys2 {0}
- 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 }}
-
- ##
- # TEST
- ##
-
- - name: Test
- if: runner.os != 'Windows'
- run: |
- ctest -E "^example64|example$" --test-dir build --output-on-failure
-
- - 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: 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
-
- - 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 }}'
- else
- APPLE_CODESIGN_ID='-'
- fi
-
- sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
- mv "PrismLauncher.app" "Prism Launcher.app"
-
- - 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' && matrix.qt_ver != 5
- 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.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
- cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
- 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.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib
- cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib
- cp /usr/lib/x86_64-linux-gnu/libffi.so.7 ${{ env.INSTALL_PORTABLE_DIR }}/lib
- mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib
-
- for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
- cd ${{ env.INSTALL_PORTABLE_DIR }}
- tar -czf ../PrismLauncher-portable.tar.gz *
-
- ##
- # UPLOAD BUILDS
- ##
-
- - name: Upload binary tarball (macOS)
- if: runner.os == 'macOS'
- uses: actions/upload-artifact@v4
- with:
- name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
- path: PrismLauncher.zip
-
- - name: Upload binary zip (Windows)
- if: runner.os == 'Windows'
- uses: actions/upload-artifact@v4
- with:
- name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}
- path: ${{ env.INSTALL_DIR }}/**
-
- - name: Upload binary zip (Windows, portable)
- if: runner.os == 'Windows'
- uses: actions/upload-artifact@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, Qt 5)
- if: runner.os == 'Linux' && matrix.qt_ver != 6
- uses: actions/upload-artifact@v4
- with:
- name: PrismLauncher-${{ runner.os }}-Qt5-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
- path: PrismLauncher-portable.tar.gz
-
- - name: Upload binary tarball (Linux, portable, Qt 6)
- if: runner.os == 'Linux' && matrix.qt_ver != 5
- uses: actions/upload-artifact@v4
- with:
- name: PrismLauncher-${{ runner.os }}-Qt6-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
- path: PrismLauncher-portable.tar.gz
-
- - name: Upload AppImage (Linux)
- if: runner.os == 'Linux' && matrix.qt_ver != 5
- uses: actions/upload-artifact@v4
- with:
- name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
- path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
-
- - name: Upload AppImage Zsync (Linux)
- if: runner.os == 'Linux' && matrix.qt_ver != 5
- uses: actions/upload-artifact@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
-
- flatpak:
- runs-on: ubuntu-latest
- container:
- image: bilelmoussaoui/flatpak-github-actions:kde-6.7
- options: --privileged
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- if: inputs.build_type == 'Debug'
- with:
- submodules: "true"
- - name: Build Flatpak (Linux)
- if: inputs.build_type == 'Debug'
- uses: flatpak/flatpak-github-actions/flatpak-builder@v6
- with:
- bundle: "Prism Launcher.flatpak"
- manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml
-
- nix:
- name: Nix (${{ matrix.system }})
+ name: Build (${{ matrix.artifact-name }})
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
- system: x86_64-linux
+ artifact-name: Linux
+ base-cmake-preset: linux
- - os: macos-13
- system: x86_64-darwin
+ - os: windows-2022
+ 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
+ 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
+ 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
- system: aarch64-darwin
+ 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 }}
+
steps:
- - name: Checkout repository
+ ##
+ # SETUP
+ ##
+
+ - name: Checkout
uses: actions/checkout@v4
-
- - name: Install Nix
- uses: cachix/install-nix-action@v30
-
- # For PRs
- - name: Setup Nix Magic Cache
- uses: DeterminateSystems/magic-nix-cache-action@v8
-
- # For in-tree builds
- - name: Setup Cachix
- uses: cachix/cachix-action@v15
with:
- name: prismlauncher
- authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
+ submodules: true
- - name: Run flake checks
- run: |
- nix flake check --print-build-logs --show-trace
+ - name: Setup dependencies
+ id: setup-dependencies
+ uses: ./.github/actions/setup-dependencies
+ with:
+ build-type: ${{ inputs.build-type || 'Debug' }}
+ msystem: ${{ matrix.msystem }}
+ vcvars-arch: ${{ matrix.vcvars-arch }}
+ qt-architecture: ${{ matrix.qt-architecture }}
- - name: Build debug package
- if: ${{ inputs.build_type == 'Debug' }}
- run: |
- nix build --print-build-logs .#prismlauncher-debug
+ ##
+ # BUILD
+ ##
- - name: Build release package
- if: ${{ inputs.build_type != 'Debug' }}
+ - 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: |
- nix build --print-build-logs .#prismlauncher
+ echo preset="$BASE_CMAKE_PRESET"_"$PRESET_TYPE" >> "$GITHUB_OUTPUT"
+
+ - name: Run CMake workflow
+ env:
+ CMAKE_PRESET: ${{ steps.cmake-preset.outputs.preset }}
+ run: |
+ cmake --workflow --preset "$CMAKE_PRESET"
+
+ ##
+ # PACKAGE
+ ##
+
+ - name: Get short version
+ id: short-version
+ shell: bash
+ run: |
+ echo "version=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
+
+ - 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 }}
+
+ gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
+ gpg-private-key-id: ${{ secrets.GPG_PRIVATE_KEY_ID }}
+
+ - name: Package (macOS)
+ if: ${{ runner.os == 'macOS' }}
+ uses: ./.github/actions/package/macos
+ with:
+ version: ${{ steps.short-version.outputs.version }}
+ build-type: ${{ steps.setup-dependencies.outputs.build-type }}
+ artifact-name: ${{ matrix.artifact-name }}
+
+ 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:
+ version: ${{ steps.short-version.outputs.version }}
+ build-type: ${{ steps.setup-dependencies.outputs.build-type }}
+ artifact-name: ${{ matrix.artifact-name }}
+ msystem: ${{ matrix.msystem }}
+
+ 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 5255f865b..5a2ecbd6d 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -1,16 +1,60 @@
name: "CodeQL Code Scanning"
-on: [ push, pull_request, workflow_dispatch ]
+on:
+ push:
+ paths:
+ # File types
+ - "**.cpp"
+ - "**.h"
+ - "**.java"
+
+ # Directories
+ - "buildconfig/"
+ - "cmake/"
+ - "launcher/"
+ - "libraries/"
+ - "program_info/"
+ - "tests/"
+
+ # Files
+ - "CMakeLists.txt"
+ - "COPYING.md"
+
+ # Workflows
+ - ".github/codeql"
+ - ".github/workflows/codeql.yml"
+ pull_request:
+ paths:
+ # File types
+ - "**.cpp"
+ - "**.h"
+
+ # Directories
+ - "buildconfig/"
+ - "cmake/"
+ - "launcher/"
+ - "libraries/"
+ - "program_info/"
+ - "tests/"
+
+ # Files
+ - "CMakeLists.txt"
+ - "COPYING.md"
+
+ # Workflows
+ - ".github/codeql"
+ - ".github/workflows/codeql.yml"
+ workflow_dispatch:
jobs:
CodeQL:
runs-on: ubuntu-latest
-
+
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
- submodules: 'true'
+ submodules: "true"
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
@@ -19,17 +63,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 qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 libqt5networkauth5 libqt5networkauth5-dev
+ - name: Setup dependencies
+ uses: ./.github/actions/setup-dependencies
+ with:
+ build-type: Debug
- name: Configure and Build
run: |
- cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr -DLauncher_QT_VERSION_MAJOR=5 -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
new file mode 100644
index 000000000..cab0edeb7
--- /dev/null
+++ b/.github/workflows/flatpak.yml
@@ -0,0 +1,95 @@
+name: Flatpak
+
+on:
+ push:
+ # We don't do anything with these artifacts on releases. They go to Flathub
+ tags-ignore:
+ - "*"
+ paths:
+ # File types
+ - "**.cpp"
+ - "**.h"
+ - "**.java"
+
+ # Build files
+ - "flatpak/"
+
+ # Directories
+ - "buildconfig/"
+ - "cmake/"
+ - "launcher/"
+ - "libraries/"
+ - "program_info/"
+ - "tests/"
+
+ # Files
+ - "CMakeLists.txt"
+ - "COPYING.md"
+
+ # Workflows
+ - ".github/workflows/flatpak.yml"
+ pull_request:
+ paths:
+ # File types
+ - "**.cpp"
+ - "**.h"
+
+ # Build files
+ - "flatpak/"
+
+ # Directories
+ - "buildconfig/"
+ - "cmake/"
+ - "launcher/"
+ - "libraries/"
+ - "program_info/"
+ - "tests/"
+
+ # Files
+ - "CMakeLists.txt"
+ - "COPYING.md"
+
+ # Workflows
+ - ".github/workflows/flatpak.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ name: Build (${{ matrix.arch }})
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: ubuntu-22.04
+ arch: x86_64
+
+ - os: ubuntu-22.04-arm
+ arch: aarch64
+
+ runs-on: ${{ matrix.os }}
+
+ container:
+ image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.8
+ options: --privileged
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ submodules: true
+
+ - name: Set short version
+ shell: bash
+ run: |
+ echo "VERSION=${GITHUB_SHA::7}" >> "$GITHUB_ENV"
+
+ - name: Build Flatpak
+ uses: flatpak/flatpak-github-actions/flatpak-builder@v6
+ with:
+ bundle: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-Flatpak.flatpak
+ manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml
+ arch: ${{ matrix.arch }}
diff --git a/.github/workflows/merge-blocking-pr.yml b/.github/workflows/merge-blocking-pr.yml
new file mode 100644
index 000000000..d37c33761
--- /dev/null
+++ b/.github/workflows/merge-blocking-pr.yml
@@ -0,0 +1,62 @@
+name: Merged Blocking Pull Request Automation
+
+on:
+ pull_request_target:
+ types:
+ - closed
+ workflow_dispatch:
+ inputs:
+ pr_id:
+ description: Local Pull Request number to work on
+ required: true
+ type: number
+
+jobs:
+ update-blocked-status:
+ name: Update Blocked Status
+ runs-on: ubuntu-latest
+
+ # a pr that was a `blocking:` label was merged.
+ # find the open pr's it was blocked by and trigger a refresh of their state
+ if: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'blocking') }}
+
+ steps:
+ - name: Generate token
+ id: generate-token
+ uses: actions/create-github-app-token@v2
+ with:
+ app-id: ${{ vars.PULL_REQUEST_APP_ID }}
+ private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }}
+
+ - name: Gather Dependent PRs
+ id: gather_deps
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ PR_NUMBER: ${{ inputs.pr_id || github.event.pull_request.number }}
+ run: |
+ blocked_prs=$(
+ gh -R ${{ github.repository }} pr list --label 'blocked' --json 'number,body' \
+ | jq -c --argjson pr "$PR_NUMBER" '
+ reduce ( .[] | select(
+ .body |
+ scan("(?:blocked (?:by|on)|stacked on):? #([0-9]+)") |
+ map(tonumber) |
+ any(.[]; . == $pr)
+ )) as $i ([]; . + [$i])
+ '
+ )
+ {
+ echo "deps=$blocked_prs"
+ echo "numdeps=$(jq -r '. | length' <<< "$blocked_prs")"
+ } >> "$GITHUB_OUTPUT"
+
+ - name: Trigger Blocked PR Workflows for Dependants
+ if: fromJSON(steps.gather_deps.outputs.numdeps) > 0
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ DEPS: ${{ steps.gather_deps.outputs.deps }}
+ run: |
+ while read -r pr ; do
+ gh -R ${{ github.repository }} workflow run 'blocked-prs.yml' -r "${{ github.ref_name }}" -f pr_id="$pr"
+ done < <(jq -c '.[].number' <<< "$DEPS")
+
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
new file mode 100644
index 000000000..5a40ebb1f
--- /dev/null
+++ b/.github/workflows/nix.yml
@@ -0,0 +1,143 @@
+name: Nix
+
+on:
+ push:
+ tags:
+ - "*"
+ paths:
+ # File types
+ - "**.cpp"
+ - "**.h"
+ - "**.java"
+
+ # Build files
+ - "**.nix"
+ - "nix/"
+ - "flake.lock"
+
+ # Directories
+ - "buildconfig/"
+ - "cmake/"
+ - "launcher/"
+ - "libraries/"
+ - "program_info/"
+ - "tests/"
+
+ # Files
+ - "CMakeLists.txt"
+ - "COPYING.md"
+
+ # Workflows
+ - ".github/workflows/nix.yml"
+ pull_request_target:
+ paths:
+ # File types
+ - "**.cpp"
+ - "**.h"
+
+ # Build files
+ - "**.nix"
+ - "nix/"
+ - "flake.lock"
+
+ # Directories
+ - "buildconfig/"
+ - "cmake/"
+ - "launcher/"
+ - "libraries/"
+ - "program_info/"
+ - "tests/"
+
+ # Files
+ - "CMakeLists.txt"
+ - "COPYING.md"
+
+ # Workflows
+ - ".github/workflows/nix.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+env:
+ DEBUG: ${{ github.ref_type != 'tag' }}
+ USE_DETERMINATE: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ build:
+ name: Build (${{ matrix.system }})
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: ubuntu-22.04
+ system: x86_64-linux
+
+ - os: ubuntu-22.04-arm
+ system: aarch64-linux
+
+ - os: macos-13
+ system: x86_64-darwin
+
+ - os: macos-14
+ system: aarch64-darwin
+
+ runs-on: ${{ matrix.os }}
+
+ permissions:
+ id-token: write
+
+ steps:
+ - name: Get merge commit
+ if: ${{ github.event_name == 'pull_request_target' }}
+ id: merge-commit
+ uses: PrismLauncher/PrismLauncher/.github/actions/get-merge-commit@develop
+ with:
+ pull-request-id: ${{ github.event.number }}
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ steps.merge-commit.outputs.merge-commit-sha || github.sha }}
+
+ - name: Install Nix
+ uses: DeterminateSystems/nix-installer-action@v17
+ with:
+ determinate: ${{ env.USE_DETERMINATE }}
+
+ # For PRs
+ - name: Setup Nix Magic Cache
+ if: ${{ env.USE_DETERMINATE == 'true' }}
+ uses: DeterminateSystems/flakehub-cache-action@v1
+
+ # For in-tree builds
+ - name: Setup Cachix
+ if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
+ uses: cachix/cachix-action@v16
+ with:
+ name: prismlauncher
+ authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
+
+ - name: Run Flake checks
+ run: |
+ nix flake check --print-build-logs --show-trace
+
+ - name: Build debug package
+ if: ${{ env.DEBUG == 'true' }}
+ run: |
+ nix build \
+ --no-link --print-build-logs --print-out-paths \
+ .#prismlauncher-debug >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Build release package
+ if: ${{ env.DEBUG == 'false' }}
+ env:
+ TAG: ${{ github.ref_name }}
+ SYSTEM: ${{ matrix.system }}
+ run: |
+ nix build --no-link --print-out-paths .#prismlauncher \
+ | tee -a "$GITHUB_STEP_SUMMARY" \
+ | xargs cachix pin prismlauncher "$TAG"-"$SYSTEM"
diff --git a/.github/workflows/winget.yml b/.github/workflows/publish.yml
similarity index 63%
rename from .github/workflows/winget.yml
rename to .github/workflows/publish.yml
index eacf23099..8a7da812e 100644
--- a/.github/workflows/winget.yml
+++ b/.github/workflows/publish.yml
@@ -1,13 +1,21 @@
-name: Publish to WinGet
+name: Publish
+
on:
release:
- types: [released]
+ types: [ released ]
+
+permissions:
+ contents: read
jobs:
- publish:
+ winget:
+ name: Winget
+
runs-on: windows-latest
+
steps:
- - uses: vedantmgoyal2009/winget-releaser@v2
+ - name: Publish on Winget
+ uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: PrismLauncher.PrismLauncher
version: ${{ github.event.release.tag_name }}
diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/release.yml
similarity index 76%
rename from .github/workflows/trigger_release.yml
rename to .github/workflows/release.yml
index e800653e3..a93233dab 100644
--- a/.github/workflows/trigger_release.yml
+++ b/.github/workflows/release.yml
@@ -10,21 +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 }}
- CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
- 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
@@ -47,10 +34,8 @@ jobs:
run: |
mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }}
mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz
- mv PrismLauncher-Linux-Qt5-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz
mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage
mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync
- mv PrismLauncher-macOS-Legacy*/PrismLauncher.zip PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip
mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip
tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}
@@ -91,13 +76,15 @@ jobs:
draft: true
prerelease: false
files: |
- PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz
PrismLauncher-Linux-x86_64.AppImage
PrismLauncher-Linux-x86_64.AppImage.zsync
PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz
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
@@ -105,5 +92,4 @@ jobs:
PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe
PrismLauncher-macOS-${{ env.VERSION }}.zip
- PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip
PrismLauncher-${{ env.VERSION }}.tar.gz
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
new file mode 100644
index 000000000..106a7844f
--- /dev/null
+++ b/.github/workflows/stale.yml
@@ -0,0 +1,29 @@
+name: Stale
+
+on:
+ schedule:
+ # run weekly on sunday
+ - cron: "0 0 * * 0"
+ workflow_dispatch:
+
+jobs:
+ label:
+ name: Label issues and PRs
+
+ runs-on: ubuntu-latest
+
+ permissions:
+ issues: write
+ pull-requests: write
+
+ steps:
+ - uses: actions/stale@v9
+ with:
+ days-before-stale: 60
+ days-before-close: -1 # Don't close anything
+ exempt-issue-labels: rfc,nostale,help wanted
+ exempt-all-milestones: true
+ exempt-all-assignees: true
+ operations-per-run: 1000
+ stale-issue-label: inactive
+ stale-pr-label: inactive
diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml
deleted file mode 100644
index 0b8386d69..000000000
--- a/.github/workflows/trigger_builds.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-name: Build Application
-
-on:
- push:
- branches-ignore:
- - "renovate/**"
- paths-ignore:
- - "**.md"
- - "**/LICENSE"
- - "flake.lock"
- - "packages/**"
- - ".github/ISSUE_TEMPLATE/**"
- - ".markdownlint**"
- pull_request:
- paths-ignore:
- - "**.md"
- - "**/LICENSE"
- - "flake.lock"
- - "packages/**"
- - ".github/ISSUE_TEMPLATE/**"
- - ".markdownlint**"
- 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 }}
- CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
- 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 5e978f356..62852171b 100644
--- a/.github/workflows/update-flake.yml
+++ b/.github/workflows/update-flake.yml
@@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- - uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
+ - uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
- uses: DeterminateSystems/update-flake-lock@v24
with:
diff --git a/.gitignore b/.gitignore
index b5523f685..00afabbfa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ CMakeLists.txt.user.*
CMakeSettings.json
/CMakeFiles
CMakeCache.txt
+CMakeUserPresets.json
/.project
/.settings
/.idea
@@ -21,6 +22,7 @@ CMakeCache.txt
/.vs
cmake-build-*/
Debug
+compile_commands.json
# Build dirs
build
@@ -47,8 +49,12 @@ run/
# Nix/NixOS
.direnv/
-.pre-commit-config.yaml
+## Used when manually invoking stdenv phases
+outputs/
+## Regular artifacts
result
+result-*
+repl-result-*
# Flatpak
.flatpak-builder
diff --git a/.gitmodules b/.gitmodules
index 0f437d277..0c56d8768 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,9 +4,6 @@
[submodule "libraries/tomlplusplus"]
path = libraries/tomlplusplus
url = https://github.com/marzer/tomlplusplus.git
-[submodule "libraries/filesystem"]
- path = libraries/filesystem
- url = https://github.com/gulrak/filesystem
[submodule "libraries/libnbtplusplus"]
path = libraries/libnbtplusplus
url = https://github.com/PrismLauncher/libnbtplusplus.git
@@ -22,3 +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
+ url = https://github.com/nayuki/QR-Code-generator
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b677b0b7c..68d900c27 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -78,13 +78,18 @@ else()
# ATL's pack list needs more than the default 1 Mib stack on windows
if(WIN32)
set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}")
+
+ # -ffunction-sections and -fdata-sections help reduce binary size
+ # -mguard=cf enables Control Flow Guard
+ # TODO: Look into -gc-sections to further reduce binary size
+ foreach(lang C CXX)
+ set("CMAKE_${lang}_FLAGS_RELEASE" "-ffunction-sections -fdata-sections -mguard=cf")
+ endforeach()
endif()
endif()
-# Fix build with Qt 5.13
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y")
-
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_WARN_DEPRECATED_UP_TO=0x060200")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_UP_TO=0x060000")
# Fix aarch64 build for toml++
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0")
@@ -92,6 +97,12 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0")
# set CXXFLAGS for build targets
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}")
+# Export compile commands for debug builds if we can (useful in LSPs like clangd)
+# https://cmake.org/cmake/help/v3.31/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html
+if(CMAKE_GENERATOR STREQUAL "Unix Makefiles" OR CMAKE_GENERATOR STREQUAL "Ninja" AND CMAKE_BUILD_TYPE STREQUAL "Debug")
+ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+endif()
+
option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF)
# If this is a Debug build turn on address sanitiser
@@ -106,14 +117,14 @@ if ((CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebI
else()
# AppleClang and Clang
message(STATUS "Address Sanitizer available on Clang")
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=null")
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=null")
endif()
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
# GCC
message(STATUS "Address Sanitizer available on GCC")
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover")
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover")
link_libraries("asan")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
message(STATUS "Address Sanitizer available on MSVC")
@@ -180,12 +191,13 @@ set(Launcher_LOGIN_CALLBACK_URL "https://prismlauncher.org/successful-login" CAC
set(Launcher_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for FML Libraries.")
######## Set version numbers ########
-set(Launcher_VERSION_MAJOR 9)
+set(Launcher_VERSION_MAJOR 10)
set(Launcher_VERSION_MINOR 0)
+set(Launcher_VERSION_PATCH 0)
-set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
-set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.0.0")
-set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},0,0")
+set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}")
+set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}.0")
+set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},${Launcher_VERSION_PATCH},0")
# Build platform.
set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.")
@@ -229,7 +241,7 @@ set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT ON)
# differing Linux/BSD/etc distributions. Downstream packagers should be explicitly opt-ing into this
# feature if they know it will work with their distribution.
if(UNIX AND NOT APPLE)
- set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF)
+ set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF)
endif()
# Java downloader
@@ -296,23 +308,11 @@ endif()
# Find the required Qt parts
include(QtVersionlessBackport)
-if(Launcher_QT_VERSION_MAJOR EQUAL 5)
- set(QT_VERSION_MAJOR 5)
- find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml NetworkAuth)
-
- if(NOT Launcher_FORCE_BUNDLED_LIBS)
- find_package(QuaZip-Qt5 1.3 QUIET)
- endif()
- if (NOT QuaZip-Qt5_FOUND)
- set(QUAZIP_QT_MAJOR_VERSION ${QT_VERSION_MAJOR} CACHE STRING "Qt version to use (4, 5 or 6), defaults to ${QT_VERSION_MAJOR}" FORCE)
- set(FORCE_BUNDLED_QUAZIP 1)
- endif()
-
- # Qt 6 sets these by default. Notably causes Windows APIs to use UNICODE strings.
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE")
-elseif(Launcher_QT_VERSION_MAJOR EQUAL 6)
+if(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_VERSION_MAJOR 6)
- find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth)
+ find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth OpenGL)
+ find_package(Qt6 COMPONENTS DBus)
+ list(APPEND Launcher_QT_DBUS Qt6::DBus)
list(APPEND Launcher_QT_LIBS Qt6::Core5Compat)
if(NOT Launcher_FORCE_BUNDLED_LIBS)
@@ -326,29 +326,16 @@ else()
message(FATAL_ERROR "Qt version ${Launcher_QT_VERSION_MAJOR} is not supported")
endif()
-if(Launcher_QT_VERSION_MAJOR EQUAL 5)
- include(ECMQueryQt)
- ecm_query_qt(QT_PLUGINS_DIR QT_INSTALL_PLUGINS)
- ecm_query_qt(QT_LIBS_DIR QT_INSTALL_LIBS)
- ecm_query_qt(QT_LIBEXECS_DIR QT_INSTALL_LIBEXECS)
-else()
+if(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_PLUGINS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_PLUGINS})
set(QT_LIBS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBS})
set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS})
endif()
-# NOTE: Qt 6 already sets this by default
-if (Qt5_POSITION_INDEPENDENT_CODE)
- SET(CMAKE_POSITION_INDEPENDENT_CODE ON)
-endif()
-
if(NOT Launcher_FORCE_BUNDLED_LIBS)
# Find toml++
find_package(tomlplusplus 3.2.0 QUIET)
- # Find ghc_filesystem
- find_package(ghc_filesystem QUIET)
-
# Find cmark
find_package(cmark QUIET)
endif()
@@ -366,7 +353,7 @@ set(Launcher_ENABLE_UPDATER NO)
set(Launcher_BUILD_UPDATER NO)
if (NOT APPLE AND (NOT Launcher_UPDATER_GITHUB_REPO STREQUAL "" AND NOT Launcher_BUILD_ARTIFACT STREQUAL ""))
- set(Launcher_BUILD_UPDATER YES)
+ set(Launcher_BUILD_UPDATER YES)
endif()
if(NOT (UNIX AND APPLE))
@@ -397,8 +384,8 @@ if(UNIX AND APPLE)
set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed")
set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed")
- set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.5.2/Sparkle-2.5.2.tar.xz" CACHE STRING "URL to Sparkle release archive")
- set(MACOSX_SPARKLE_SHA256 "572dd67ae398a466f19f343a449e1890bac1ef74885b4739f68f979a8a89884b" CACHE STRING "SHA256 checksum for Sparkle release archive")
+ set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz" CACHE STRING "URL to Sparkle release archive")
+ set(MACOSX_SPARKLE_SHA256 "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b" CACHE STRING "SHA256 checksum for Sparkle release archive")
set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle")
# directories to look for dependencies
@@ -488,6 +475,7 @@ 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")
@@ -543,12 +531,6 @@ else()
endif()
add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
-if (NOT ghc_filesystem_FOUND)
- message(STATUS "Using bundled ghc_filesystem")
- add_subdirectory(libraries/filesystem) # Implementation of std::filesystem for old C++, for usage in old macOS
-else()
- message(STATUS "Using system ghc_filesystem")
-endif()
add_subdirectory(libraries/qdcss) # css parser
############################### Built Artifacts ###############################
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/CONTRIBUTING.md b/CONTRIBUTING.md
index 072916772..5965f4d8e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,16 +2,59 @@
## Code formatting
-Try to follow the existing formatting.
-If there is no existing formatting, you may use `clang-format` with our included `.clang-format` configuration.
+All files are formatted with `clang-format` using the configuration in `.clang-format`. Ensure it is run on changed files before committing!
-In general, in order of importance:
+Please also follow the project's conventions for C++:
-- Make sure your IDE is not messing up line endings or whitespace and avoid using linters.
-- Prefer readability over dogma.
-- Keep to the existing formatting.
-- Indent with 4 space unless it's in a submodule.
-- Keep lists (of arguments, parameters, initializers...) as lists, not paragraphs. It should either read from top to bottom, or left to right. Not both.
+- Class and type names should be formatted as `PascalCase`: `MyClass`.
+- Private or protected class data members should be formatted as `camelCase` prefixed with `m_`: `m_myCounter`.
+- Private or protected `static` class data members should be formatted as `camelCase` prefixed with `s_`: `s_instance`.
+- Public class data members should be formatted as `camelCase` without the prefix: `dateOfBirth`.
+- Public, private or protected `static const` class data members should be formatted as `SCREAMING_SNAKE_CASE`: `MAX_VALUE`.
+- Class function members should be formatted as `camelCase` without a prefix: `incrementCounter`.
+- Global functions and non-`const` global variables should be formatted as `camelCase` without a prefix: `globalData`.
+- `const` global variables, macros, and enum constants should be formatted as `SCREAMING_SNAKE_CASE`: `LIGHT_GRAY`.
+- Avoid inventing acronyms or abbreviations especially for a name of multiple words - like `tp` for `texturePack`.
+
+Most of these rules are included in the `.clang-tidy` file, so you can run `clang-tidy` to check for any violations.
+
+Here is what these conventions with the formatting configuration look like:
+
+```c++
+#define AWESOMENESS 10
+
+constexpr double PI = 3.14159;
+
+enum class PizzaToppings { HAM_AND_PINEAPPLE, OREO_AND_KETCHUP };
+
+struct Person {
+ QString name;
+ QDateTime dateOfBirth;
+
+ long daysOld() const { return dateOfBirth.daysTo(QDateTime::currentDateTime()); }
+};
+
+class ImportantClass {
+ public:
+ void incrementCounter()
+ {
+ if (m_counter + 1 > MAX_COUNTER_VALUE)
+ throw std::runtime_error("Counter has reached limit!");
+
+ ++m_counter;
+ }
+
+ int counter() const { return m_counter; }
+
+ private:
+ static constexpr int MAX_COUNTER_VALUE = 100;
+ int m_counter;
+};
+
+ImportantClass importantClassInstance;
+```
+
+If you see any names which do not follow these conventions, it is preferred that you leave them be - renames increase the number of changes therefore make reviewing harder and make your PR more prone to conflicts. However, if you're refactoring a whole class anyway, it's fine.
## Signing your work
diff --git a/COPYING.md b/COPYING.md
index 111587060..1ebde116f 100644
--- a/COPYING.md
+++ b/COPYING.md
@@ -1,7 +1,7 @@
## Prism Launcher
Prism Launcher - Minecraft Launcher
- Copyright (C) 2022-2024 Prism Launcher Contributors
+ Copyright (C) 2022-2025 Prism Launcher Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -108,7 +108,7 @@
Information on third party licenses used in MinGW-w64 can be found in its COPYING.MinGW-w64-runtime.txt.
-## Qt 5/6
+## Qt 6
Copyright (C) 2022 The Qt Company Ltd and other contributors.
Contact: https://www.qt.io/licensing
@@ -362,28 +362,6 @@
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
-## gulrak/filesystem
-
- Copyright (c) 2018, Steffen Schümann
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
-
## Breeze icons
Copyright (C) 2014 Uri Herrera and others
@@ -425,3 +403,12 @@
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`)
+
+ Copyright © 2024 Project Nayuki. (MIT License)
+ https://www.nayuki.io/page/qr-code-generator-library
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+ - The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in
index b48232b43..6bebcb80e 100644
--- a/buildconfig/BuildConfig.cpp.in
+++ b/buildconfig/BuildConfig.cpp.in
@@ -34,8 +34,8 @@
*/
#include
-#include "BuildConfig.h"
#include
+#include "BuildConfig.h"
const Config BuildConfig;
@@ -49,7 +49,7 @@ Config::Config()
LAUNCHER_DOMAIN = "@Launcher_Domain@";
LAUNCHER_CONFIGFILE = "@Launcher_ConfigFile@";
LAUNCHER_GIT = "@Launcher_Git@";
- LAUNCHER_DESKTOPFILENAME = "@Launcher_DesktopFileName@";
+ LAUNCHER_APPID = "@Launcher_AppID@";
LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@";
USER_AGENT = "@Launcher_UserAgent@";
@@ -58,6 +58,7 @@ Config::Config()
// Version information
VERSION_MAJOR = @Launcher_VERSION_MAJOR@;
VERSION_MINOR = @Launcher_VERSION_MINOR@;
+ VERSION_PATCH = @Launcher_VERSION_PATCH@;
BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@";
BUILD_ARTIFACT = "@Launcher_BUILD_ARTIFACT@";
@@ -74,14 +75,13 @@ Config::Config()
MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@";
MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@";
- if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty())
- {
+ if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) {
UPDATER_ENABLED = true;
- } else if(!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) {
+ } else if (!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) {
UPDATER_ENABLED = true;
}
- #cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER
+#cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER
JAVA_DOWNLOADER_ENABLED = Launcher_ENABLE_JAVA_DOWNLOADER;
GIT_COMMIT = "@Launcher_GIT_COMMIT@";
@@ -89,27 +89,19 @@ Config::Config()
GIT_REFSPEC = "@Launcher_GIT_REFSPEC@";
// Assume that builds outside of Git repos are "stable"
- if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND")
- || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND")
- || GIT_REFSPEC == QStringLiteral("")
- || GIT_TAG == QStringLiteral("GIT-NOTFOUND"))
- {
+ if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") ||
+ GIT_REFSPEC == QStringLiteral("") || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) {
GIT_REFSPEC = "refs/heads/stable";
GIT_TAG = versionString();
GIT_COMMIT = "";
}
- if (GIT_REFSPEC.startsWith("refs/heads/"))
- {
+ if (GIT_REFSPEC.startsWith("refs/heads/")) {
VERSION_CHANNEL = GIT_REFSPEC;
- VERSION_CHANNEL.remove("refs/heads/");
- }
- else if (!GIT_COMMIT.isEmpty())
- {
+ VERSION_CHANNEL.remove("refs/heads/");
+ } else if (!GIT_COMMIT.isEmpty()) {
VERSION_CHANNEL = GIT_COMMIT.mid(0, 8);
- }
- else
- {
+ } else {
VERSION_CHANNEL = "unknown";
}
@@ -136,7 +128,7 @@ Config::Config()
QString Config::versionString() const
{
- return QString("%1.%2").arg(VERSION_MAJOR).arg(VERSION_MINOR);
+ return QString("%1.%2.%3").arg(VERSION_MAJOR).arg(VERSION_MINOR).arg(VERSION_PATCH);
}
QString Config::printableVersionString() const
@@ -144,8 +136,7 @@ QString Config::printableVersionString() const
QString vstr = versionString();
// If the build is not a main release, append the channel
- if(VERSION_CHANNEL != "stable" && GIT_TAG != vstr)
- {
+ if (VERSION_CHANNEL != "stable" && GIT_TAG != vstr) {
vstr += "-" + VERSION_CHANNEL;
}
return vstr;
@@ -162,4 +153,3 @@ QString Config::systemID() const
{
return QStringLiteral("%1 %2 %3").arg(COMPILER_TARGET_SYSTEM, COMPILER_TARGET_SYSTEM_VERSION, COMPILER_TARGET_SYSTEM_PROCESSOR);
}
-
diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h
index ae705d098..b59adcb57 100644
--- a/buildconfig/BuildConfig.h
+++ b/buildconfig/BuildConfig.h
@@ -52,13 +52,15 @@ class Config {
QString LAUNCHER_DOMAIN;
QString LAUNCHER_CONFIGFILE;
QString LAUNCHER_GIT;
- QString LAUNCHER_DESKTOPFILENAME;
+ QString LAUNCHER_APPID;
QString LAUNCHER_SVGFILENAME;
/// The major version number.
int VERSION_MAJOR;
/// The minor version number.
int VERSION_MINOR;
+ /// The patch version number.
+ int VERSION_PATCH;
/**
* The version channel
diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in
index 6d3845dfc..3a8c8fbfe 100644
--- a/cmake/MacOSXBundleInfo.plist.in
+++ b/cmake/MacOSXBundleInfo.plist.in
@@ -8,6 +8,8 @@
A Minecraft mod wants to access your microphone.
NSDownloadsFolderUsageDescription
Prism uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where Prism scans for downloaded mods in Settings or the prompt that appears.
+ NSLocalNetworkUsageDescription
+ Minecraft uses the local network to find and connect to LAN servers.
NSPrincipalClass
NSApplication
NSHighResolutionCapable
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/default.nix b/default.nix
index 6466507b7..5ecef5590 100644
--- a/default.nix
+++ b/default.nix
@@ -1,9 +1,4 @@
-(import (
- let
- lock = builtins.fromJSON (builtins.readFile ./flake.lock);
- in
- fetchTarball {
- url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
- sha256 = lock.nodes.flake-compat.locked.narHash;
- }
-) { src = ./.; }).defaultNix
+(import (fetchTarball {
+ url = "https://github.com/edolstra/flake-compat/archive/ff81ac966bb2cae68946d5ed5fc4994f96d0ffec.tar.gz";
+ sha256 = "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=";
+}) { src = ./.; }).defaultNix
diff --git a/flake.lock b/flake.lock
index a82e6f65f..5418557a3 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,29 +1,13 @@
{
"nodes": {
- "flake-compat": {
- "flake": false,
- "locked": {
- "lastModified": 1696426674,
- "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
- "owner": "edolstra",
- "repo": "flake-compat",
- "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
- "type": "github"
- },
- "original": {
- "owner": "edolstra",
- "repo": "flake-compat",
- "type": "github"
- }
- },
"libnbtplusplus": {
"flake": false,
"locked": {
- "lastModified": 1699286814,
- "narHash": "sha256-yy0q+bky80LtK1GWzz7qpM+aAGrOqLuewbid8WT1ilk=",
+ "lastModified": 1744811532,
+ "narHash": "sha256-qhmjaRkt+O7A+gu6HjUkl7QzOEb4r8y8vWZMG2R/C6o=",
"owner": "PrismLauncher",
"repo": "libnbtplusplus",
- "rev": "23b955121b8217c1c348a9ed2483167a6f3ff4ad",
+ "rev": "531449ba1c930c98e0bcf5d332b237a8566f9d78",
"type": "github"
},
"original": {
@@ -32,28 +16,13 @@
"type": "github"
}
},
- "nix-filter": {
- "locked": {
- "lastModified": 1710156097,
- "narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
- "owner": "numtide",
- "repo": "nix-filter",
- "rev": "3342559a24e85fc164b295c3444e8a139924675b",
- "type": "github"
- },
- "original": {
- "owner": "numtide",
- "repo": "nix-filter",
- "type": "github"
- }
- },
"nixpkgs": {
"locked": {
- "lastModified": 1729256560,
- "narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=",
+ "lastModified": 1745526057,
+ "narHash": "sha256-ITSpPDwvLBZBnPRS2bUcHY3gZSwis/uTe255QgMtTLA=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0",
+ "rev": "f771eb401a46846c1aebd20552521b233dd7e18b",
"type": "github"
},
"original": {
@@ -63,12 +32,27 @@
"type": "github"
}
},
+ "qt-qrcodegenerator": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1737616857,
+ "narHash": "sha256-6SugPt0lp1Gz7nV23FLmsmpfzgFItkSw7jpGftsDPWc=",
+ "owner": "nayuki",
+ "repo": "QR-Code-generator",
+ "rev": "2c9044de6b049ca25cb3cd1649ed7e27aa055138",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nayuki",
+ "repo": "QR-Code-generator",
+ "type": "github"
+ }
+ },
"root": {
"inputs": {
- "flake-compat": "flake-compat",
"libnbtplusplus": "libnbtplusplus",
- "nix-filter": "nix-filter",
- "nixpkgs": "nixpkgs"
+ "nixpkgs": "nixpkgs",
+ "qt-qrcodegenerator": "qt-qrcodegenerator"
}
}
},
diff --git a/flake.nix b/flake.nix
index f4ca782ec..69abd78dd 100644
--- a/flake.nix
+++ b/flake.nix
@@ -16,25 +16,8 @@
flake = false;
};
- nix-filter.url = "github:numtide/nix-filter";
-
- /*
- Inputs below this are optional and can be removed
-
- ```
- {
- inputs.prismlauncher = {
- url = "github:PrismLauncher/PrismLauncher";
- inputs = {
- flake-compat.follows = "";
- };
- };
- }
- ```
- */
-
- flake-compat = {
- url = "github:edolstra/flake-compat";
+ qt-qrcodegenerator = {
+ url = "github:nayuki/QR-Code-generator";
flake = false;
};
};
@@ -44,9 +27,9 @@
self,
nixpkgs,
libnbtplusplus,
- nix-filter,
- ...
+ qt-qrcodegenerator,
}:
+
let
inherit (nixpkgs) lib;
@@ -58,53 +41,151 @@
forAllSystems = lib.genAttrs systems;
nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system});
in
+
{
checks = forAllSystems (
system:
+
let
- checks' = nixpkgsFor.${system}.callPackage ./nix/checks.nix { inherit self; };
+ pkgs = nixpkgsFor.${system};
+ llvm = pkgs.llvmPackages_19;
in
- lib.filterAttrs (_: lib.isDerivation) checks'
+
+ {
+ formatting =
+ pkgs.runCommand "check-formatting"
+ {
+ nativeBuildInputs = with pkgs; [
+ deadnix
+ llvm.clang-tools
+ markdownlint-cli
+ nixfmt-rfc-style
+ statix
+ ];
+ }
+ ''
+ cd ${self}
+
+ echo "Running clang-format...."
+ clang-format --dry-run --style='file' --Werror */**.{c,cc,cpp,h,hh,hpp}
+
+ echo "Running deadnix..."
+ deadnix --fail
+
+ echo "Running markdownlint..."
+ markdownlint --dot .
+
+ echo "Running nixfmt..."
+ find -type f -name '*.nix' -exec nixfmt --check {} +
+
+ echo "Running statix"
+ statix check .
+
+ touch $out
+ '';
+ }
);
devShells = forAllSystems (
system:
+
let
pkgs = nixpkgsFor.${system};
+ llvm = pkgs.llvmPackages_19;
+
+ packages' = self.packages.${system};
+
+ welcomeMessage = ''
+ Welcome to the Prism Launcher repository! 🌈
+
+ We just set some things up for you. To get building, you can run:
+
+ ```
+ $ cd "$cmakeBuildDir"
+ $ ninjaBuildPhase
+ $ ninjaInstallPhase
+ ```
+
+ Feel free to ask any questions in our Discord server or Matrix space:
+ - https://prismlauncher.org/discord
+ - https://matrix.to/#/#prismlauncher:matrix.org
+
+ And thanks for helping out :)
+ '';
+
+ # Re-use our package wrapper to wrap our development environment
+ qt-wrapper-env = packages'.prismlauncher.overrideAttrs (old: {
+ name = "qt-wrapper-env";
+
+ # Required to use script-based makeWrapper below
+ strictDeps = true;
+
+ # We don't need/want the unwrapped Prism package
+ paths = [ ];
+
+ nativeBuildInputs = old.nativeBuildInputs or [ ] ++ [
+ # Ensure the wrapper is script based so it can be sourced
+ pkgs.makeWrapper
+ ];
+
+ # Inspired by https://discourse.nixos.org/t/python-qt-woes/11808/10
+ buildCommand = ''
+ makeQtWrapper ${lib.getExe pkgs.runtimeShellPackage} "$out"
+ sed -i '/^exec/d' "$out"
+ '';
+ });
in
+
{
default = pkgs.mkShell {
- inputsFrom = [ self.packages.${system}.prismlauncher-unwrapped ];
- buildInputs = with pkgs; [
+ name = "prism-launcher";
+
+ inputsFrom = [ packages'.prismlauncher-unwrapped ];
+
+ packages = with pkgs; [
ccache
- ninja
+ llvm.clang-tools
];
+
+ cmakeBuildType = "Debug";
+ cmakeFlags = [ "-GNinja" ] ++ packages'.prismlauncher.cmakeFlags;
+ dontFixCmake = true;
+
+ shellHook = ''
+ echo "Sourcing ${qt-wrapper-env}"
+ source ${qt-wrapper-env}
+
+ git submodule update --init --force
+
+ if [ ! -f compile_commands.json ]; then
+ cmakeConfigurePhase
+ cd ..
+ ln -s "$cmakeBuildDir"/compile_commands.json compile_commands.json
+ fi
+
+ echo ${lib.escapeShellArg welcomeMessage}
+ '';
};
}
);
formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style);
- overlays.default =
- final: prev:
- let
- version = builtins.substring 0 8 self.lastModifiedDate or "dirty";
- in
- {
- prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix {
- inherit
- libnbtplusplus
- nix-filter
- self
- version
- ;
- };
-
- prismlauncher = final.callPackage ./nix/wrapper.nix { };
+ overlays.default = final: prev: {
+ prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix {
+ inherit
+ libnbtplusplus
+ qt-qrcodegenerator
+ self
+ ;
};
+ prismlauncher = final.callPackage ./nix/wrapper.nix { };
+ };
+
packages = forAllSystems (
system:
+
let
pkgs = nixpkgsFor.${system};
@@ -117,6 +198,7 @@
default = prismPackages.prismlauncher;
};
in
+
# Only output them if they're available on the current system
lib.filterAttrs (_: lib.meta.availableOn pkgs.stdenv.hostPlatform) packages
);
@@ -124,16 +206,18 @@
# We put these under legacyPackages as they are meant for CI, not end user consumption
legacyPackages = forAllSystems (
system:
+
let
- prismPackages = self.packages.${system};
- legacyPackages = self.legacyPackages.${system};
+ packages' = self.packages.${system};
+ legacyPackages' = self.legacyPackages.${system};
in
+
{
- prismlauncher-debug = prismPackages.prismlauncher.override {
- prismlauncher-unwrapped = legacyPackages.prismlauncher-unwrapped-debug;
+ prismlauncher-debug = packages'.prismlauncher.override {
+ prismlauncher-unwrapped = legacyPackages'.prismlauncher-unwrapped-debug;
};
- prismlauncher-unwrapped-debug = prismPackages.prismlauncher-unwrapped.overrideAttrs {
+ prismlauncher-unwrapped-debug = packages'.prismlauncher-unwrapped.overrideAttrs {
cmakeBuildType = "Debug";
dontStrip = true;
};
diff --git a/flatpak/flite.json b/flatpak/flite.json
new file mode 100644
index 000000000..1bf280af1
--- /dev/null
+++ b/flatpak/flite.json
@@ -0,0 +1,20 @@
+{
+ "name": "flite",
+ "config-opts": [
+ "--enable-shared",
+ "--with-audio=pulseaudio"
+ ],
+ "no-parallel-make": true,
+ "sources": [
+ {
+ "type": "git",
+ "url": "https://github.com/festvox/flite.git",
+ "tag": "v2.2",
+ "commit": "e9e2e37c329dbe98bfeb27a1828ef9a71fa84f88",
+ "x-checker-data": {
+ "type": "git",
+ "tag-pattern": "^v([\\d.]+)$"
+ }
+ }
+ ]
+}
diff --git a/flatpak/libdecor.json b/flatpak/libdecor.json
index 589310a35..1652a2f04 100644
--- a/flatpak/libdecor.json
+++ b/flatpak/libdecor.json
@@ -1,22 +1,18 @@
{
- "name": "libdecor",
- "buildsystem": "meson",
- "config-opts": [
- "-Ddemo=false"
- ],
- "sources": [
- {
- "type": "git",
- "url": "https://gitlab.freedesktop.org/libdecor/libdecor.git",
- "commit": "73260393a97291c887e1074ab7f318e031be0ac6"
- },
- {
- "type": "patch",
- "path": "patches/weird_libdecor.patch"
- }
- ],
- "cleanup": [
- "/include",
- "/lib/pkgconfig"
- ]
+ "name": "libdecor",
+ "buildsystem": "meson",
+ "config-opts": [
+ "-Ddemo=false"
+ ],
+ "sources": [
+ {
+ "type": "git",
+ "url": "https://gitlab.freedesktop.org/libdecor/libdecor.git",
+ "commit": "c2bd8ad6fa42c0cb17553ce77ad8a87d1f543b1f"
+ }
+ ],
+ "cleanup": [
+ "/include",
+ "/lib/pkgconfig"
+ ]
}
diff --git a/flatpak/org.prismlauncher.PrismLauncher.yml b/flatpak/org.prismlauncher.PrismLauncher.yml
index 09dd8d73b..136aef91a 100644
--- a/flatpak/org.prismlauncher.PrismLauncher.yml
+++ b/flatpak/org.prismlauncher.PrismLauncher.yml
@@ -1,6 +1,6 @@
id: org.prismlauncher.PrismLauncher
runtime: org.kde.Platform
-runtime-version: 6.7
+runtime-version: '6.8'
sdk: org.kde.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.openjdk17
@@ -19,6 +19,12 @@ finish-args:
- --filesystem=xdg-download:ro
# FTBApp import
- --filesystem=~/.ftba:ro
+ # Userspace visibility for manual hugepages configuration
+ # Required for -XX:+UseLargePages
+ - --filesystem=/sys/kernel/mm/hugepages:ro
+ # Userspace visibility for transparent hugepages configuration
+ # Required for -XX:+UseTransparentHugePages
+ - --filesystem=/sys/kernel/mm/transparent_hugepage:ro
modules:
# Might be needed by some Controller mods (see https://github.com/isXander/Controlify/issues/31)
@@ -27,11 +33,16 @@ modules:
# Needed for proper Wayland support
- libdecor.json
+ # Text to Speech in the game
+ - flite.json
+
- name: prismlauncher
buildsystem: cmake-ninja
builddir: true
config-opts:
- -DLauncher_BUILD_PLATFORM=flatpak
+ # This allows us to manage and update Java independently of this Flatpak
+ - -DLauncher_ENABLE_JAVA_DOWNLOADER=ON
- -DCMAKE_BUILD_TYPE=RelWithDebInfo
build-options:
env:
@@ -47,18 +58,14 @@ modules:
config-opts:
- -DCMAKE_BUILD_TYPE=RelWithDebInfo
- -DBUILD_SHARED_LIBS:BOOL=ON
- - -DGLFW_USE_WAYLAND:BOOL=ON
+ - -DGLFW_BUILD_WAYLAND:BOOL=ON
- -DGLFW_BUILD_DOCS:BOOL=OFF
sources:
- type: git
url: https://github.com/glfw/glfw.git
- commit: 3fa2360720eeba1964df3c0ecf4b5df8648a8e52
+ commit: 7b6aead9fb88b3623e3b3725ebb42670cbe4c579 # 3.4
- type: patch
- path: patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch
- - type: patch
- path: patches/0005-Add-warning-about-being-an-unofficial-patch.patch
- - type: patch
- path: patches/0007-Platform-Prefer-Wayland-over-X11.patch
+ path: patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch
cleanup:
- /include
- /lib/cmake
@@ -68,8 +75,8 @@ modules:
buildsystem: autotools
sources:
- type: archive
- url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.2.tar.xz
- sha256: c8bee4790d9058bacc4b6246456c58021db58a87ddda1a9d0139bf5f18f1f240
+ url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.3.tar.xz
+ sha256: f8dd7566adb74147fab9964680b6bbadee87cf406a7fcff51718a5e6949b841c
x-checker-data:
type: anitya
project-id: 14957
@@ -91,8 +98,8 @@ modules:
sources:
- type: archive
dest-filename: gamemode.tar.gz
- url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.8.1
- sha256: 969cf85b5ca3944f3e315cd73a0ee9bea4f9c968cd7d485e9f4745bc1e679c4e
+ url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.8.2
+ sha256: 2886d4ce543c78bd2a364316d5e7fd59ef06b71de63f896b37c6d3dc97658f60
x-checker-data:
type: json
url: https://api.github.com/repos/FeralInteractive/gamemode/releases/latest
diff --git a/flatpak/patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch b/flatpak/patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch
deleted file mode 100644
index 9130e856c..000000000
--- a/flatpak/patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch
+++ /dev/null
@@ -1,24 +0,0 @@
-diff --git a/src/wl_window.c b/src/wl_window.c
-index 52d3b9eb..4ac4eb5d 100644
---- a/src/wl_window.c
-+++ b/src/wl_window.c
-@@ -2117,8 +2117,7 @@ void _glfwSetWindowTitleWayland(_GLFWwindow* window, const char* title)
- void _glfwSetWindowIconWayland(_GLFWwindow* window,
- int count, const GLFWimage* images)
- {
-- _glfwInputError(GLFW_FEATURE_UNAVAILABLE,
-- "Wayland: The platform does not support setting the window icon");
-+ fprintf(stderr, "!!! Ignoring Error: Wayland: The platform does not support setting the window icon\n");
- }
-
- void _glfwGetWindowPosWayland(_GLFWwindow* window, int* xpos, int* ypos)
-@@ -2361,8 +2360,7 @@ void _glfwRequestWindowAttentionWayland(_GLFWwindow* window)
-
- void _glfwFocusWindowWayland(_GLFWwindow* window)
- {
-- _glfwInputError(GLFW_FEATURE_UNAVAILABLE,
-- "Wayland: The platform does not support setting the input focus");
-+ fprintf(stderr, "!!! Ignoring Error: Wayland: The platform does not support setting the input focus\n");
- }
-
- void _glfwSetWindowMonitorWayland(_GLFWwindow* window,
diff --git a/flatpak/patches/0005-Add-warning-about-being-an-unofficial-patch.patch b/flatpak/patches/0005-Add-warning-about-being-an-unofficial-patch.patch
deleted file mode 100644
index b031d739f..000000000
--- a/flatpak/patches/0005-Add-warning-about-being-an-unofficial-patch.patch
+++ /dev/null
@@ -1,17 +0,0 @@
-diff --git a/src/init.c b/src/init.c
-index 06dbb3f2..a7c6da86 100644
---- a/src/init.c
-+++ b/src/init.c
-@@ -449,6 +449,12 @@ GLFWAPI int glfwInit(void)
- _glfw.initialized = GLFW_TRUE;
-
- glfwDefaultWindowHints();
-+
-+ fprintf(stderr, "!!! Patched GLFW from https://github.com/Admicos/minecraft-wayland\n"
-+ "!!! If any issues with the window, or some issues with rendering, occur, "
-+ "first try with the built-in GLFW, and if that solves the issue, report there first.\n"
-+ "!!! Use outside Minecraft is untested, and things might break.\n");
-+
- return GLFW_TRUE;
- }
-
diff --git a/flatpak/patches/0007-Platform-Prefer-Wayland-over-X11.patch b/flatpak/patches/0007-Platform-Prefer-Wayland-over-X11.patch
deleted file mode 100644
index 4eeb81309..000000000
--- a/flatpak/patches/0007-Platform-Prefer-Wayland-over-X11.patch
+++ /dev/null
@@ -1,20 +0,0 @@
-diff --git a/src/platform.c b/src/platform.c
-index c5966ae7..3e7442f9 100644
---- a/src/platform.c
-+++ b/src/platform.c
-@@ -49,12 +49,12 @@ static const struct
- #if defined(_GLFW_COCOA)
- { GLFW_PLATFORM_COCOA, _glfwConnectCocoa },
- #endif
--#if defined(_GLFW_X11)
-- { GLFW_PLATFORM_X11, _glfwConnectX11 },
--#endif
- #if defined(_GLFW_WAYLAND)
- { GLFW_PLATFORM_WAYLAND, _glfwConnectWayland },
- #endif
-+#if defined(_GLFW_X11)
-+ { GLFW_PLATFORM_X11, _glfwConnectX11 },
-+#endif
- };
-
- GLFWbool _glfwSelectPlatform(int desiredID, _GLFWplatform* platform)
diff --git a/flatpak/patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch b/flatpak/patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch
new file mode 100644
index 000000000..70cec9981
--- /dev/null
+++ b/flatpak/patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch
@@ -0,0 +1,59 @@
+From 9997ae55a47de469ea26f8437c30b51483abda5f Mon Sep 17 00:00:00 2001
+From: Dan Klishch
+Date: Sat, 30 Sep 2023 23:38:05 -0400
+Subject: Defer setting cursor position until the cursor is locked
+
+---
+ src/wl_platform.h | 3 +++
+ src/wl_window.c | 14 ++++++++++++--
+ 2 files changed, 15 insertions(+), 2 deletions(-)
+
+diff --git a/src/wl_platform.h b/src/wl_platform.h
+index ca34f66e..cd1f227f 100644
+--- a/src/wl_platform.h
++++ b/src/wl_platform.h
+@@ -403,6 +403,9 @@ typedef struct _GLFWwindowWayland
+ int scaleSize;
+ int compositorPreferredScale;
+
++ double askedCursorPosX, askedCursorPosY;
++ GLFWbool didAskForSetCursorPos;
++
+ struct zwp_relative_pointer_v1* relativePointer;
+ struct zwp_locked_pointer_v1* lockedPointer;
+ struct zwp_confined_pointer_v1* confinedPointer;
+diff --git a/src/wl_window.c b/src/wl_window.c
+index 1de26558..0df16747 100644
+--- a/src/wl_window.c
++++ b/src/wl_window.c
+@@ -2586,8 +2586,9 @@ void _glfwGetCursorPosWayland(_GLFWwindow* window, double* xpos, double* ypos)
+
+ void _glfwSetCursorPosWayland(_GLFWwindow* window, double x, double y)
+ {
+- _glfwInputError(GLFW_FEATURE_UNAVAILABLE,
+- "Wayland: The platform does not support setting the cursor position");
++ window->wl.didAskForSetCursorPos = true;
++ window->wl.askedCursorPosX = x;
++ window->wl.askedCursorPosY = y;
+ }
+
+ void _glfwSetCursorModeWayland(_GLFWwindow* window, int mode)
+@@ -2819,6 +2820,15 @@ static const struct zwp_relative_pointer_v1_listener relativePointerListener =
+ static void lockedPointerHandleLocked(void* userData,
+ struct zwp_locked_pointer_v1* lockedPointer)
+ {
++ _GLFWwindow* window = userData;
++
++ if (window->wl.didAskForSetCursorPos)
++ {
++ window->wl.didAskForSetCursorPos = false;
++ zwp_locked_pointer_v1_set_cursor_position_hint(window->wl.lockedPointer,
++ wl_fixed_from_double(window->wl.askedCursorPosX),
++ wl_fixed_from_double(window->wl.askedCursorPosY));
++ }
+ }
+
+ static void lockedPointerHandleUnlocked(void* userData,
+--
+2.42.0
+
diff --git a/flatpak/patches/weird_libdecor.patch b/flatpak/patches/weird_libdecor.patch
deleted file mode 100644
index 3a400b820..000000000
--- a/flatpak/patches/weird_libdecor.patch
+++ /dev/null
@@ -1,40 +0,0 @@
-diff --git a/src/libdecor.c b/src/libdecor.c
-index a9c1106..1aa38b3 100644
---- a/src/libdecor.c
-+++ b/src/libdecor.c
-@@ -1391,22 +1391,32 @@ calculate_priority(const struct libdecor_plugin_description *plugin_description)
- static bool
- check_symbol_conflicts(const struct libdecor_plugin_description *plugin_description)
- {
-+ bool ret = true;
- char * const *symbol;
-+ void* main_prog = dlopen(NULL, RTLD_LAZY);
-+ if (!main_prog) {
-+ fprintf(stderr, "Plugin \"%s\" couldn't check conflicting symbols: \"%s\".\n",
-+ plugin_description->description, dlerror());
-+ return false;
-+ }
-+
-
- symbol = plugin_description->conflicting_symbols;
- while (*symbol) {
- dlerror();
-- dlsym (RTLD_DEFAULT, *symbol);
-+ dlsym (main_prog, *symbol);
- if (!dlerror()) {
- fprintf(stderr, "Plugin \"%s\" uses conflicting symbol \"%s\".\n",
- plugin_description->description, *symbol);
-- return false;
-+ ret = false;
-+ break;
- }
-
- symbol++;
- }
-
-- return true;
-+ dlclose(main_prog);
-+ return ret;
- }
-
- static struct plugin_loader *
diff --git a/flatpak/shared-modules b/flatpak/shared-modules
index f2b0c16a2..73f08ed2c 160000
--- a/flatpak/shared-modules
+++ b/flatpak/shared-modules
@@ -1 +1 @@
-Subproject commit f2b0c16a2a217a1822ce5a6538ba8f755ed1dd32
+Subproject commit 73f08ed2c3187f6648ca04ebef030930a6c9f0be
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index ea749ca4c..0daab026c 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -48,6 +48,7 @@
#include "net/PasteUpload.h"
#include "pathmatcher/MultiMatcher.h"
#include "pathmatcher/SimplePrefixMatcher.h"
+#include "tasks/Task.h"
#include "tools/GenericProfiler.h"
#include "ui/InstanceWindow.h"
#include "ui/MainWindow.h"
@@ -58,8 +59,6 @@
#include "ui/pages/BasePageProvider.h"
#include "ui/pages/global/APIPage.h"
#include "ui/pages/global/AccountListPage.h"
-#include "ui/pages/global/CustomCommandsPage.h"
-#include "ui/pages/global/EnvironmentVariablesPage.h"
#include "ui/pages/global/ExternalToolsPage.h"
#include "ui/pages/global/JavaPage.h"
#include "ui/pages/global/LanguagePage.h"
@@ -97,6 +96,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -128,6 +128,7 @@
#include
#include
+#include
#include "SysInfo.h"
#ifdef Q_OS_LINUX
@@ -154,9 +155,15 @@
#endif
#if defined Q_OS_WIN32
-#include
-#include "WindowsConsole.h"
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
#endif
+#include
+#include
+#include "console/WindowsConsole.h"
+#endif
+
+#include "console/Console.h"
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
@@ -165,6 +172,63 @@ static const QLatin1String liveCheckFile("live.check");
PixmapCache* PixmapCache::s_instance = nullptr;
+static bool isANSIColorConsole;
+
+static QString defaultLogFormat = QStringLiteral(
+ "%{time process}"
+ " "
+ "%{if-debug}Debug:%{endif}"
+ "%{if-info}Info:%{endif}"
+ "%{if-warning}Warning:%{endif}"
+ "%{if-critical}Critical:%{endif}"
+ "%{if-fatal}Fatal:%{endif}"
+ " "
+ "%{if-category}[%{category}] %{endif}"
+ "%{message}"
+ " "
+ "(%{function}:%{line})");
+
+#define ansi_reset "\x1b[0m"
+#define ansi_bold "\x1b[1m"
+#define ansi_reset_bold "\x1b[22m"
+#define ansi_faint "\x1b[2m"
+#define ansi_italic "\x1b[3m"
+#define ansi_red_fg "\x1b[31m"
+#define ansi_green_fg "\x1b[32m"
+#define ansi_yellow_fg "\x1b[33m"
+#define ansi_blue_fg "\x1b[34m"
+#define ansi_purple_fg "\x1b[35m"
+#define ansi_inverse "\x1b[7m"
+
+// clang-format off
+static QString ansiLogFormat = QStringLiteral(
+ ansi_faint "%{time process}" ansi_reset
+ " "
+ "%{if-debug}" ansi_bold ansi_green_fg "D:" ansi_reset "%{endif}"
+ "%{if-info}" ansi_bold ansi_blue_fg "I:" ansi_reset "%{endif}"
+ "%{if-warning}" ansi_bold ansi_yellow_fg "W:" ansi_reset_bold "%{endif}"
+ "%{if-critical}" ansi_bold ansi_red_fg "C:" ansi_reset_bold "%{endif}"
+ "%{if-fatal}" ansi_bold ansi_inverse ansi_red_fg "F:" ansi_reset_bold "%{endif}"
+ " "
+ "%{if-category}" ansi_bold "[%{category}]" ansi_reset_bold " %{endif}"
+ "%{message}"
+ " "
+ ansi_reset ansi_faint "(%{function}:%{line})" ansi_reset
+);
+// clang-format on
+
+#undef ansi_inverse
+#undef ansi_purple_fg
+#undef ansi_blue_fg
+#undef ansi_yellow_fg
+#undef ansi_green_fg
+#undef ansi_red_fg
+#undef ansi_italic
+#undef ansi_faint
+#undef ansi_bold
+#undef ansi_reset_bold
+#undef ansi_reset
+
namespace {
/** This is used so that we can output to the log file in addition to the CLI. */
@@ -173,11 +237,24 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt
static std::mutex loggerMutex;
const std::lock_guard lock(loggerMutex); // synchronized, QFile logFile is not thread-safe
+ if (isANSIColorConsole) {
+ // ensure default is set for log file
+ qSetMessagePattern(defaultLogFormat);
+ }
+
QString out = qFormatLogMessage(type, context, msg);
out += QChar::LineFeed;
APPLICATION->logFile->write(out.toUtf8());
APPLICATION->logFile->flush();
+
+ if (isANSIColorConsole) {
+ // format ansi for console;
+ qSetMessagePattern(ansiLogFormat);
+ out = qFormatLogMessage(type, context, msg);
+ out += QChar::LineFeed;
+ }
+
QTextStream(stderr) << out.toLocal8Bit();
fflush(stderr);
}
@@ -218,15 +295,25 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// attach the parent console if stdout not already captured
if (AttachWindowsConsole()) {
consoleAttached = true;
+ if (auto err = EnableAnsiSupport(); !err) {
+ isANSIColorConsole = true;
+ } else {
+ std::cout << "Error setting up ansi console" << err.message() << std::endl;
+ }
+ }
+#else
+ if (console::isConsole()) {
+ isANSIColorConsole = true;
}
#endif
+
setOrganizationName(BuildConfig.LAUNCHER_NAME);
setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN);
setApplicationName(BuildConfig.LAUNCHER_NAME);
setApplicationDisplayName(QString("%1 %2").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()));
setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT);
- setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME);
- startTime = QDateTime::currentDateTime();
+ setDesktopFileName(BuildConfig.LAUNCHER_APPID);
+ m_startTime = QDateTime::currentDateTime();
// Don't quit on hiding the last window
this->setQuitOnLastWindowClosed(false);
@@ -242,6 +329,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
{ { "s", "server" }, "Join the specified server on launch (only valid in combination with --launch)", "address" },
{ { "w", "world" }, "Join the specified world on launch (only valid in combination with --launch)", "world" },
{ { "a", "profile" }, "Use the account specified by its profile name (only valid in combination with --launch)", "profile" },
+ { { "o", "offline" }, "Launch offline, with given player name (only valid in combination with --launch)", "offline" },
{ "alive", "Write a small '" + liveCheckFile + "' file after the launcher starts" },
{ { "I", "import" }, "Import instance or resource from specified local path or URL", "url" },
{ "show", "Opens the window for the specified instance (by instance ID)", "show" } });
@@ -257,6 +345,10 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_serverToJoin = parser.value("server");
m_worldToJoin = parser.value("world");
m_profileToUse = parser.value("profile");
+ if (parser.isSet("offline")) {
+ m_offline = true;
+ m_offlineName = parser.value("offline");
+ }
m_liveCheck = parser.isSet("alive");
m_instanceIdToShowWindowOf = parser.value("show");
@@ -271,8 +363,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
}
// error if --launch is missing with --server or --profile
- if (((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty()) || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty()) {
- std::cerr << "--server and --profile can only be used in combination with --launch!" << std::endl;
+ if ((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty() || !m_profileToUse.isEmpty() || m_offline) &&
+ m_instanceIdToLaunch.isEmpty()) {
+ std::cerr << "--server, --profile and --offline can only be used in combination with --launch!" << std::endl;
m_status = Application::Failed;
return;
}
@@ -369,19 +462,20 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_peerInstance = new LocalPeer(this, appID);
connect(m_peerInstance, &LocalPeer::messageReceived, this, &Application::messageReceived);
if (m_peerInstance->isClient()) {
+ bool sentMessage = false;
int timeout = 2000;
if (m_instanceIdToLaunch.isEmpty()) {
ApplicationMessage activate;
activate.command = "activate";
- m_peerInstance->sendMessage(activate.serialize(), timeout);
+ sentMessage = m_peerInstance->sendMessage(activate.serialize(), timeout);
if (!m_urlsToImport.isEmpty()) {
for (auto url : m_urlsToImport) {
ApplicationMessage import;
import.command = "import";
import.args.insert("url", url.toString());
- m_peerInstance->sendMessage(import.serialize(), timeout);
+ sentMessage = m_peerInstance->sendMessage(import.serialize(), timeout);
}
}
} else {
@@ -397,10 +491,20 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
if (!m_profileToUse.isEmpty()) {
launch.args["profile"] = m_profileToUse;
}
- m_peerInstance->sendMessage(launch.serialize(), timeout);
+ if (m_offline) {
+ launch.args["offline_enabled"] = "true";
+ launch.args["offline_name"] = m_offlineName;
+ }
+ sentMessage = m_peerInstance->sendMessage(launch.serialize(), timeout);
+ }
+ if (sentMessage) {
+ m_status = Application::Succeeded;
+ return;
+ } else {
+ std::cerr << "Unable to redirect command to already running instance\n";
+ // C function not Qt function - event loop not started yet
+ ::exit(1);
}
- m_status = Application::Succeeded;
- return;
}
}
@@ -431,27 +535,14 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
return;
}
qInstallMessageHandler(appDebugOutput);
-
- qSetMessagePattern(
- "%{time process}"
- " "
- "%{if-debug}D%{endif}"
- "%{if-info}I%{endif}"
- "%{if-warning}W%{endif}"
- "%{if-critical}C%{endif}"
- "%{if-fatal}F%{endif}"
- " "
- "|"
- " "
- "%{if-category}[%{category}]: %{endif}"
- "%{message}");
+ qSetMessagePattern(defaultLogFormat);
bool foundLoggingRules = false;
auto logRulesFile = QStringLiteral("qtlogging.ini");
auto logRulesPath = FS::PathCombine(dataPath, logRulesFile);
- qDebug() << "Testing" << logRulesPath << "...";
+ qInfo() << "Testing" << logRulesPath << "...";
foundLoggingRules = QFile::exists(logRulesPath);
// search the dataPath()
@@ -459,7 +550,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) {
logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile));
if (!logRulesPath.isEmpty()) {
- qDebug() << "Found" << logRulesPath << "...";
+ qInfo() << "Found" << logRulesPath << "...";
foundLoggingRules = true;
}
}
@@ -470,28 +561,28 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
#else
logRulesPath = FS::PathCombine(m_rootPath, logRulesFile);
#endif
- qDebug() << "Testing" << logRulesPath << "...";
+ qInfo() << "Testing" << logRulesPath << "...";
foundLoggingRules = QFile::exists(logRulesPath);
}
if (foundLoggingRules) {
// load and set logging rules
- qDebug() << "Loading logging rules from:" << logRulesPath;
+ qInfo() << "Loading logging rules from:" << logRulesPath;
QSettings loggingRules(logRulesPath, QSettings::IniFormat);
loggingRules.beginGroup("Rules");
QStringList rule_names = loggingRules.childKeys();
QStringList rules;
- qDebug() << "Setting log rules:";
+ qInfo() << "Setting log rules:";
for (auto rule_name : rule_names) {
auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString());
rules.append(rule);
- qDebug() << " " << rule;
+ qInfo() << " " << rule;
}
auto rules_str = rules.join("\n");
QLoggingCategory::setFilterRules(rules_str);
}
- qDebug() << "<> Log initialized.";
+ qInfo() << "<> Log initialized.";
}
{
@@ -508,33 +599,33 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
}
{
- qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
- qDebug() << "Version : " << BuildConfig.printableVersionString();
- qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM;
- qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
- qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
- qDebug() << "Compiled for : " << BuildConfig.systemID();
- qDebug() << "Compiled by : " << BuildConfig.compilerID();
- qDebug() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT;
- qDebug() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No");
+ qInfo() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
+ qInfo() << "Version : " << BuildConfig.printableVersionString();
+ qInfo() << "Platform : " << BuildConfig.BUILD_PLATFORM;
+ qInfo() << "Git commit : " << BuildConfig.GIT_COMMIT;
+ qInfo() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
+ qInfo() << "Compiled for : " << BuildConfig.systemID();
+ qInfo() << "Compiled by : " << BuildConfig.compilerID();
+ qInfo() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT;
+ qInfo() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No");
if (adjustedBy.size()) {
- qDebug() << "Work dir before adjustment : " << origcwdPath;
- qDebug() << "Work dir after adjustment : " << QDir::currentPath();
- qDebug() << "Adjusted by : " << adjustedBy;
+ qInfo() << "Work dir before adjustment : " << origcwdPath;
+ qInfo() << "Work dir after adjustment : " << QDir::currentPath();
+ qInfo() << "Adjusted by : " << adjustedBy;
} else {
- qDebug() << "Work dir : " << QDir::currentPath();
+ qInfo() << "Work dir : " << QDir::currentPath();
}
- qDebug() << "Binary path : " << binPath;
- qDebug() << "Application root path : " << m_rootPath;
+ qInfo() << "Binary path : " << binPath;
+ qInfo() << "Application root path : " << m_rootPath;
if (!m_instanceIdToLaunch.isEmpty()) {
- qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch;
+ qInfo() << "ID of instance to launch : " << m_instanceIdToLaunch;
}
if (!m_serverToJoin.isEmpty()) {
- qDebug() << "Address of server to join :" << m_serverToJoin;
+ qInfo() << "Address of server to join :" << m_serverToJoin;
} else if (!m_worldToJoin.isEmpty()) {
- qDebug() << "Name of the world to join :" << m_worldToJoin;
+ qInfo() << "Name of the world to join :" << m_worldToJoin;
}
- qDebug() << "<> Paths set.";
+ qInfo() << "<> Paths set.";
}
if (m_liveCheck) {
@@ -605,6 +696,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("IconsDir", "icons");
m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
m_settings->registerSetting("DownloadsDirWatchRecursive", false);
+ m_settings->registerSetting("MoveModsFromDownloadsDir", false);
m_settings->registerSetting("SkinsDir", "skins");
m_settings->registerSetting("JavaDir", "java");
@@ -698,7 +790,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("ToolbarsLocked", false);
+ // Instance
m_settings->registerSetting("InstSortMode", "Name");
+ m_settings->registerSetting("InstRenamingMode", "AskEverytime");
m_settings->registerSetting("SelectedInstance", QString());
// Window state and geometry
@@ -790,8 +884,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_globalSettingsProvider->addPage();
m_globalSettingsProvider->addPage();
m_globalSettingsProvider->addPage();
- m_globalSettingsProvider->addPage();
- m_globalSettingsProvider->addPage();
m_globalSettingsProvider->addPage();
m_globalSettingsProvider->addPage();
m_globalSettingsProvider->addPage();
@@ -800,7 +892,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
PixmapCache::setInstance(new PixmapCache(this));
- qDebug() << "<> Settings loaded.";
+ qInfo() << "<> Settings loaded.";
}
#ifndef QT_NO_ACCESSIBILITY
@@ -816,7 +908,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
QString user = settings()->get("ProxyUser").toString();
QString pass = settings()->get("ProxyPass").toString();
updateProxySettings(proxyTypeStr, addr, port, user, pass);
- qDebug() << "<> Network done.";
+ qInfo() << "<> Network done.";
}
// load translations
@@ -824,8 +916,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_translations.reset(new TranslationsModel("translations"));
auto bcp47Name = m_settings->get("Language").toString();
m_translations->selectLanguage(bcp47Name);
- qDebug() << "Your language is" << bcp47Name;
- qDebug() << "<> Translations loaded.";
+ qInfo() << "Your language is" << bcp47Name;
+ qInfo() << "<> Translations loaded.";
}
// Instance icons
@@ -835,8 +927,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
":/icons/multimc/128x128/instances/", ":/icons/multimc/scalable/instances/" };
m_icons.reset(new IconList(instFolders, setting->get().toString()));
connect(setting.get(), &Setting::SettingChanged,
- [&](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); });
- qDebug() << "<> Instance icons initialized.";
+ [this](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); });
+ qInfo() << "<> Instance icons initialized.";
}
// Themes
@@ -848,25 +940,25 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// instance path: check for problems with '!' in instance path and warn the user in the log
// and remember that we have to show him a dialog when the gui starts (if it does so)
QString instDir = InstDirSetting->get().toString();
- qDebug() << "Instance path : " << instDir;
+ qInfo() << "Instance path : " << instDir;
if (FS::checkProblemticPathJava(QDir(instDir))) {
qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!";
}
m_instances.reset(new InstanceList(m_settings, instDir, this));
connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged);
- qDebug() << "Loading Instances...";
+ qInfo() << "Loading Instances...";
m_instances->loadList();
- qDebug() << "<> Instances loaded.";
+ qInfo() << "<> Instances loaded.";
}
// and accounts
{
m_accounts.reset(new AccountList(this));
- qDebug() << "Loading accounts...";
+ qInfo() << "Loading accounts...";
m_accounts->setListFilePath("accounts.json", true);
m_accounts->loadList();
m_accounts->fillQueue();
- qDebug() << "<> Accounts loaded.";
+ qInfo() << "<> Accounts loaded.";
}
// init the http meta cache
@@ -887,7 +979,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_metacache->addBase("meta", QDir("meta").absolutePath());
m_metacache->addBase("java", QDir("cache/java").absolutePath());
m_metacache->Load();
- qDebug() << "<> Cache initialized.";
+ qInfo() << "<> Cache initialized.";
}
// now we have network, download translation updates
@@ -1070,11 +1162,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
bool Application::createSetupWizard()
{
- bool javaRequired = [&]() {
- if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_settings->get("AutomaticJavaDownload").toBool()) {
+ bool javaRequired = [this]() {
+ if (BuildConfig.JAVA_DOWNLOADER_ENABLED && settings()->get("AutomaticJavaDownload").toBool()) {
return false;
}
- bool ignoreJavaWizard = m_settings->get("IgnoreJavaWizard").toBool();
+ bool ignoreJavaWizard = settings()->get("IgnoreJavaWizard").toBool();
if (ignoreJavaWizard) {
return false;
}
@@ -1088,8 +1180,8 @@ bool Application::createSetupWizard()
QString actualPath = FS::ResolveExecutable(currentJavaPath);
return actualPath.isNull();
}();
- bool askjava = BuildConfig.JAVA_DOWNLOADER_ENABLED && !javaRequired && !m_settings->get("AutomaticJavaDownload").toBool() &&
- !m_settings->get("AutomaticJavaSwitch").toBool() && !m_settings->get("UserAskedAboutAutomaticJavaDownload").toBool();
+ bool askjava = BuildConfig.JAVA_DOWNLOADER_ENABLED && !javaRequired && !settings()->get("AutomaticJavaDownload").toBool() &&
+ !settings()->get("AutomaticJavaSwitch").toBool() && !settings()->get("UserAskedAboutAutomaticJavaDownload").toBool();
bool languageRequired = settings()->get("Language").toString().isEmpty();
bool pasteInterventionRequired = settings()->get("PastebinURL") != "";
bool validWidgets = m_themeManager->isValidApplicationTheme(settings()->get("ApplicationTheme").toString());
@@ -1101,8 +1193,16 @@ bool Application::createSetupWizard()
// set default theme after going into theme wizard
if (!validIcons)
settings()->set("IconTheme", QString("pe_colored"));
- if (!validWidgets)
- settings()->set("ApplicationTheme", QString("system"));
+ if (!validWidgets) {
+#if defined(Q_OS_WIN32) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
+ const QString style =
+ QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark ? QStringLiteral("dark") : QStringLiteral("bright");
+#else
+ const QString style = QStringLiteral("system");
+#endif
+
+ settings()->set("ApplicationTheme", style);
+ }
m_themeManager->applyCurrentlySelectedTheme(true);
@@ -1169,6 +1269,9 @@ bool Application::event(QEvent* event)
#endif
if (event->type() == QEvent::FileOpen) {
+ if (!m_mainWindow) {
+ showMainWindow(false);
+ }
auto ev = static_cast(event);
m_mainWindow->processURLs({ ev->url() });
}
@@ -1209,7 +1312,7 @@ void Application::performMainStartupAction()
qDebug() << " Launching with account" << m_profileToUse;
}
- launch(inst, true, false, targetToJoin, accountToUse);
+ launch(inst, !m_offline, false, targetToJoin, accountToUse, m_offlineName);
return;
}
}
@@ -1302,12 +1405,17 @@ void Application::messageReceived(const QByteArray& message)
qWarning() << "Received" << command << "message without a zip path/URL.";
return;
}
+ if (!m_mainWindow) {
+ showMainWindow(false);
+ }
m_mainWindow->processURLs({ normalizeImportUrl(url) });
} else if (command == "launch") {
QString id = received.args["id"];
QString server = received.args["server"];
QString world = received.args["world"];
QString profile = received.args["profile"];
+ bool offline = received.args["offline_enabled"] == "true";
+ QString offlineName = received.args["offline_name"];
InstancePtr instance;
if (!id.isEmpty()) {
@@ -1337,7 +1445,7 @@ void Application::messageReceived(const QByteArray& message)
}
}
- launch(instance, true, false, serverObject, accountObject);
+ launch(instance, !offline, false, serverObject, accountObject, offlineName);
} else {
qWarning() << "Received invalid message" << message;
}
@@ -1375,11 +1483,17 @@ bool Application::openJsonEditor(const QString& filename)
}
}
-bool Application::launch(InstancePtr instance, bool online, bool demo, MinecraftTarget::Ptr targetToJoin, MinecraftAccountPtr accountToUse)
+bool Application::launch(InstancePtr instance,
+ bool online,
+ bool demo,
+ MinecraftTarget::Ptr targetToJoin,
+ MinecraftAccountPtr accountToUse,
+ const QString& offlineName)
{
if (m_updateRunning) {
qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed.";
} else if (instance->canLaunch()) {
+ QMutexLocker locker(&m_instanceExtrasMutex);
auto& extras = m_instanceExtras[instance->id()];
auto window = extras.window;
if (window) {
@@ -1395,6 +1509,7 @@ bool Application::launch(InstancePtr instance, bool online, bool demo, Minecraft
controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get());
controller->setTargetToJoin(targetToJoin);
controller->setAccountToUse(accountToUse);
+ controller->setOfflineName(offlineName);
if (window) {
controller->setParentWidget(window);
} else if (m_mainWindow) {
@@ -1404,7 +1519,7 @@ bool Application::launch(InstancePtr instance, bool online, bool demo, Minecraft
connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed);
connect(controller.get(), &LaunchController::aborted, this, [this] { controllerFailed(tr("Aborted")); });
addRunningInstance();
- controller->start();
+ QMetaObject::invokeMethod(controller.get(), &Task::start, Qt::QueuedConnection);
return true;
} else if (instance->isRunning()) {
showInstanceWindow(instance, "console");
@@ -1422,9 +1537,11 @@ bool Application::kill(InstancePtr instance)
qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running.";
return false;
}
+ QMutexLocker locker(&m_instanceExtrasMutex);
auto& extras = m_instanceExtras[instance->id()];
// NOTE: copy of the shared pointer keeps it alive
auto controller = extras.controller;
+ locker.unlock();
if (controller) {
return controller->abort();
}
@@ -1478,12 +1595,14 @@ void Application::controllerSucceeded()
if (!controller)
return;
auto id = controller->id();
+
+ QMutexLocker locker(&m_instanceExtrasMutex);
auto& extras = m_instanceExtras[id];
// on success, do...
if (controller->instance()->settings()->get("AutoCloseConsole").toBool()) {
if (extras.window) {
- extras.window->close();
+ QMetaObject::invokeMethod(extras.window, &QWidget::close, Qt::QueuedConnection);
}
}
extras.controller.reset();
@@ -1503,6 +1622,7 @@ void Application::controllerFailed(const QString& error)
if (!controller)
return;
auto id = controller->id();
+ QMutexLocker locker(&m_instanceExtrasMutex);
auto& extras = m_instanceExtras[id];
// on failure, do... nothing
@@ -1560,6 +1680,7 @@ InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString pa
if (!instance)
return nullptr;
auto id = instance->id();
+ QMutexLocker locker(&m_instanceExtrasMutex);
auto& extras = m_instanceExtras[id];
auto& window = extras.window;
@@ -1597,6 +1718,7 @@ void Application::on_windowClose()
m_openWindows--;
auto instWindow = qobject_cast(QObject::sender());
if (instWindow) {
+ QMutexLocker locker(&m_instanceExtrasMutex);
auto& extras = m_instanceExtras[instWindow->instanceId()];
extras.window = nullptr;
if (extras.controller) {
@@ -1844,7 +1966,7 @@ bool Application::handleDataMigration(const QString& currentData,
matcher->add(std::make_shared("themes/"));
ProgressDialog diag;
- DataMigrationTask task(nullptr, oldData, currentData, matcher);
+ DataMigrationTask task(oldData, currentData, matcher);
if (diag.execWithTask(&task)) {
qDebug() << "<> Migration succeeded";
setDoNotMigrate();
@@ -1910,4 +2032,4 @@ bool Application::checkQSavePath(QString path)
}
}
return false;
-}
\ No newline at end of file
+}
diff --git a/launcher/Application.h b/launcher/Application.h
index 692625f4a..12f41509c 100644
--- a/launcher/Application.h
+++ b/launcher/Application.h
@@ -112,7 +112,7 @@ class Application : public QApplication {
std::shared_ptr settings() const { return m_settings; }
- qint64 timeSinceStart() const { return startTime.msecsTo(QDateTime::currentDateTime()); }
+ qint64 timeSinceStart() const { return m_startTime.msecsTo(QDateTime::currentDateTime()); }
QIcon getThemedIcon(const QString& name);
@@ -211,7 +211,8 @@ class Application : public QApplication {
bool online = true,
bool demo = false,
MinecraftTarget::Ptr targetToJoin = nullptr,
- MinecraftAccountPtr accountToUse = nullptr);
+ MinecraftAccountPtr accountToUse = nullptr,
+ const QString& offlineName = QString());
bool kill(InstancePtr instance);
void closeCurrentWindow();
@@ -236,7 +237,7 @@ class Application : public QApplication {
bool shouldExitNow() const;
private:
- QDateTime startTime;
+ QDateTime m_startTime;
shared_qobject_ptr m_network;
@@ -279,6 +280,7 @@ class Application : public QApplication {
shared_qobject_ptr controller;
};
std::map m_instanceExtras;
+ mutable QMutex m_instanceExtrasMutex;
// main state variables
size_t m_openWindows = 0;
@@ -300,6 +302,8 @@ class Application : public QApplication {
QString m_serverToJoin;
QString m_worldToJoin;
QString m_profileToUse;
+ bool m_offline = false;
+ QString m_offlineName;
bool m_liveCheck = false;
QList m_urlsToImport;
QString m_instanceIdToShowWindowOf;
diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp
index 69cf95e3c..70e0f9dc1 100644
--- a/launcher/BaseInstance.cpp
+++ b/launcher/BaseInstance.cpp
@@ -42,8 +42,8 @@
#include
#include
#include
-#include
+#include "Application.h"
#include "settings/INISettingsObject.h"
#include "settings/OverrideSetting.h"
#include "settings/Setting.h"
@@ -174,6 +174,12 @@ void BaseInstance::copyManagedPack(BaseInstance& other)
m_settings->set("ManagedPackName", other.getManagedPackName());
m_settings->set("ManagedPackVersionID", other.getManagedPackVersionID());
m_settings->set("ManagedPackVersionName", other.getManagedPackVersionName());
+
+ if (APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() && m_settings->get("AutomaticJava").toBool() &&
+ m_settings->get("OverrideJavaLocation").toBool()) {
+ m_settings->set("OverrideJavaLocation", false);
+ m_settings->set("JavaPath", "");
+ }
}
int BaseInstance::getConsoleMaxLines() const
@@ -386,6 +392,12 @@ void BaseInstance::setName(QString val)
emit propertiesChanged(this);
}
+bool BaseInstance::syncInstanceDirName(const QString& newRoot) const
+{
+ auto oldRoot = instanceRoot();
+ return oldRoot == newRoot || QFile::rename(oldRoot, newRoot);
+}
+
QString BaseInstance::name() const
{
return m_settings->get("name").toString();
@@ -411,3 +423,8 @@ void BaseInstance::updateRuntimeContext()
{
// NOOP
}
+
+bool BaseInstance::isLegacy()
+{
+ return traits().contains("legacyLaunch") || traits().contains("alphaLaunch");
+}
diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h
index 2be28d1ec..1acf1afe0 100644
--- a/launcher/BaseInstance.h
+++ b/launcher/BaseInstance.h
@@ -126,6 +126,9 @@ class BaseInstance : public QObject, public std::enable_shared_from_this
-DataMigrationTask::DataMigrationTask(QObject* parent,
- const QString& sourcePath,
- const QString& targetPath,
- const IPathMatcher::Ptr pathMatcher)
- : Task(parent), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath)
+DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathMatcher)
+ : Task(), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath)
{
- m_copy.matcher(m_pathMatcher.get()).whitelist(true);
+ m_copy.matcher(m_pathMatcher).whitelist(true);
}
void DataMigrationTask::executeTask()
@@ -27,7 +24,7 @@ void DataMigrationTask::executeTask()
// 1. Scan
// Check how many files we gotta copy
- m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] {
+ m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] {
return m_copy(true); // dry run to collect amount of files
});
connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished);
@@ -40,11 +37,7 @@ void DataMigrationTask::dryRunFinished()
disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished);
disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::dryRunAborted);
-#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
if (!m_copyFuture.isValid() || !m_copyFuture.result()) {
-#else
- if (!m_copyFuture.result()) {
-#endif
emitFailed(tr("Failed to scan source path."));
return;
}
@@ -60,7 +53,7 @@ void DataMigrationTask::dryRunFinished()
setProgress(m_copy.totalCopied(), m_toCopy);
setStatus(tr("Copying %1…").arg(shortenedName));
});
- m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] {
+ m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] {
return m_copy(false); // actually copy now
});
connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished);
@@ -78,11 +71,7 @@ void DataMigrationTask::copyFinished()
disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished);
disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::copyAborted);
-#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
if (!m_copyFuture.isValid() || !m_copyFuture.result()) {
-#else
- if (!m_copyFuture.result()) {
-#endif
emitFailed(tr("Some paths could not be copied!"));
return;
}
diff --git a/launcher/DataMigrationTask.h b/launcher/DataMigrationTask.h
index aba9f2399..fc613cd5e 100644
--- a/launcher/DataMigrationTask.h
+++ b/launcher/DataMigrationTask.h
@@ -18,7 +18,7 @@
class DataMigrationTask : public Task {
Q_OBJECT
public:
- explicit DataMigrationTask(QObject* parent, const QString& sourcePath, const QString& targetPath, IPathMatcher::Ptr pathmatcher);
+ explicit DataMigrationTask(const QString& sourcePath, const QString& targetPath, IPathMatcher::Ptr pathmatcher);
~DataMigrationTask() override = default;
protected:
diff --git a/launcher/FileIgnoreProxy.cpp b/launcher/FileIgnoreProxy.cpp
index df06c3c75..cebe82eda 100644
--- a/launcher/FileIgnoreProxy.cpp
+++ b/launcher/FileIgnoreProxy.cpp
@@ -40,12 +40,11 @@
#include
#include
#include
-#include
#include "FileSystem.h"
#include "SeparatorPrefixTree.h"
#include "StringUtils.h"
-FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), root(root) {}
+FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), m_root(root) {}
// NOTE: Sadly, we have to do sorting ourselves.
bool FileIgnoreProxy::lessThan(const QModelIndex& left, const QModelIndex& right) const
{
@@ -104,10 +103,10 @@ QVariant FileIgnoreProxy::data(const QModelIndex& index, int role) const
if (index.column() == 0 && role == Qt::CheckStateRole) {
QFileSystemModel* fsm = qobject_cast(sourceModel());
auto blockedPath = relPath(fsm->filePath(sourceIndex));
- auto cover = blocked.cover(blockedPath);
+ auto cover = m_blocked.cover(blockedPath);
if (!cover.isNull()) {
return QVariant(Qt::Unchecked);
- } else if (blocked.exists(blockedPath)) {
+ } else if (m_blocked.exists(blockedPath)) {
return QVariant(Qt::PartiallyChecked);
} else {
return QVariant(Qt::Checked);
@@ -130,7 +129,7 @@ bool FileIgnoreProxy::setData(const QModelIndex& index, const QVariant& value, i
QString FileIgnoreProxy::relPath(const QString& path) const
{
- return QDir(root).relativeFilePath(path);
+ return QDir(m_root).relativeFilePath(path);
}
bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state)
@@ -146,18 +145,18 @@ bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state)
bool changed = false;
if (state == Qt::Unchecked) {
// blocking a path
- auto& node = blocked.insert(blockedPath);
+ auto& node = m_blocked.insert(blockedPath);
// get rid of all blocked nodes below
node.clear();
changed = true;
} else if (state == Qt::Checked || state == Qt::PartiallyChecked) {
- if (!blocked.remove(blockedPath)) {
- auto cover = blocked.cover(blockedPath);
+ if (!m_blocked.remove(blockedPath)) {
+ auto cover = m_blocked.cover(blockedPath);
qDebug() << "Blocked by cover" << cover;
// uncover
- blocked.remove(cover);
+ m_blocked.remove(cover);
// block all contents, except for any cover
- QModelIndex rootIndex = fsm->index(FS::PathCombine(root, cover));
+ QModelIndex rootIndex = fsm->index(FS::PathCombine(m_root, cover));
QModelIndex doing = rootIndex;
int row = 0;
QStack todo;
@@ -179,7 +178,7 @@ bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state)
todo.push(node);
} else {
// or just block this one.
- blocked.insert(relpath);
+ m_blocked.insert(relpath);
}
row++;
}
@@ -229,7 +228,7 @@ bool FileIgnoreProxy::shouldExpand(QModelIndex index)
return false;
}
auto blockedPath = relPath(fsm->filePath(sourceIndex));
- auto found = blocked.find(blockedPath);
+ auto found = m_blocked.find(blockedPath);
if (found) {
return !found->leaf();
}
@@ -239,8 +238,8 @@ bool FileIgnoreProxy::shouldExpand(QModelIndex index)
void FileIgnoreProxy::setBlockedPaths(QStringList paths)
{
beginResetModel();
- blocked.clear();
- blocked.insert(paths);
+ m_blocked.clear();
+ m_blocked.insert(paths);
endResetModel();
}
@@ -270,7 +269,28 @@ 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 blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(root), fileName));
+ return m_blocked.covers(relPath(file.absoluteFilePath())) || ignoreFile(file);
+}
+
+void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName)
+{
+ QFile ignoreFile(fileName);
+ if (!ignoreFile.open(QIODevice::ReadOnly)) {
+ return;
+ }
+ auto ignoreData = ignoreFile.readAll();
+ auto string = QString::fromUtf8(ignoreData);
+ setBlockedPaths(string.split('\n', Qt::SkipEmptyParts));
+}
+
+void FileIgnoreProxy::saveBlockedPathsToFile(const QString& fileName)
+{
+ auto ignoreData = blockedPaths().toStringList().join('\n').toUtf8();
+ try {
+ FS::write(fileName, ignoreData);
+ } catch (const Exception& e) {
+ qWarning() << e.cause();
+ }
}
diff --git a/launcher/FileIgnoreProxy.h b/launcher/FileIgnoreProxy.h
index e01a2651e..5184fc354 100644
--- a/launcher/FileIgnoreProxy.h
+++ b/launcher/FileIgnoreProxy.h
@@ -61,15 +61,19 @@ class FileIgnoreProxy : public QSortFilterProxyModel {
void setBlockedPaths(QStringList paths);
- inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; }
- inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; }
+ inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return m_blocked; }
+ inline SeparatorPrefixTree<'/'>& blockedPaths() { return m_blocked; }
// list of file names that need to be removed completely from model
inline QStringList& ignoreFilesWithName() { return m_ignoreFiles; }
// 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);
+
+ void saveBlockedPathsToFile(const QString& fileName);
protected:
bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const;
@@ -78,8 +82,8 @@ class FileIgnoreProxy : public QSortFilterProxyModel {
bool ignoreFile(QFileInfo file) const;
private:
- const QString root;
- SeparatorPrefixTree<'/'> blocked;
+ const QString m_root;
+ SeparatorPrefixTree<'/'> m_blocked;
QStringList m_ignoreFiles;
SeparatorPrefixTree<'/'> m_ignoreFilePaths;
};
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
index a02a0d642..a8e089f2d 100644
--- a/launcher/FileSystem.cpp
+++ b/launcher/FileSystem.cpp
@@ -77,24 +77,8 @@
#include
#endif
-// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
-
-#ifdef __APPLE__
-#include // for deployment target to support pre-catalina targets without std::fs
-#endif // __APPLE__
-
-#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
-#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
-#define GHC_USE_STD_FS
#include
namespace fs = std::filesystem;
-#endif // MacOS min version check
-#endif // Other OSes version check
-
-#ifndef GHC_USE_STD_FS
-#include
-namespace fs = ghc::filesystem;
-#endif
// clone
#if defined(Q_OS_LINUX)
@@ -341,7 +325,7 @@ bool copy::operator()(const QString& offset, bool dryRun)
opt |= copy_opts::overwrite_existing;
// Function that'll do the actual copying
- auto copy_file = [&](QString src_path, QString relative_dst_path) {
+ auto copy_file = [this, dryRun, src, dst, opt, &err](QString src_path, QString relative_dst_path) {
if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist))
return;
@@ -428,7 +412,7 @@ void create_link::make_link_list(const QString& offset)
m_recursive = true;
// Function that'll do the actual linking
- auto link_file = [&](QString src_path, QString relative_dst_path) {
+ auto link_file = [this, dst](QString src_path, QString relative_dst_path) {
if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) {
qDebug() << "path" << relative_dst_path << "in black list or not in whitelist";
return;
@@ -523,7 +507,7 @@ void create_link::runPrivileged(const QString& offset)
QString serverName = BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink_server" + StringUtils::getRandomAlphaNumeric();
- connect(&m_linkServer, &QLocalServer::newConnection, this, [&]() {
+ connect(&m_linkServer, &QLocalServer::newConnection, this, [this, &gotResults]() {
qDebug() << "Client connected, sending out pairs";
// construct block of data to send
QByteArray block;
@@ -605,7 +589,7 @@ void create_link::runPrivileged(const QString& offset)
}
ExternalLinkFileProcess* linkFileProcess = new ExternalLinkFileProcess(serverName, m_useHardLinks, this);
- connect(linkFileProcess, &ExternalLinkFileProcess::processExited, this, [&]() { emit finishedPrivileged(gotResults); });
+ connect(linkFileProcess, &ExternalLinkFileProcess::processExited, this, [this, gotResults]() { emit finishedPrivileged(gotResults); });
connect(linkFileProcess, &ExternalLinkFileProcess::finished, linkFileProcess, &QObject::deleteLater);
linkFileProcess->start();
@@ -695,9 +679,6 @@ bool deletePath(QString path)
bool trash(QString path, QString* pathInTrash)
{
-#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
- return false;
-#else
// FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal
if (DesktopServices::isFlatpak())
return false;
@@ -706,7 +687,6 @@ bool trash(QString path, QString* pathInTrash)
return false;
#endif
return QFile::moveToTrash(path, pathInTrash);
-#endif
}
QString PathCombine(const QString& path1, const QString& path2)
@@ -740,11 +720,7 @@ int pathDepth(const QString& path)
QFileInfo info(path);
-#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
- auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), QString::SkipEmptyParts);
-#else
auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), Qt::SkipEmptyParts);
-#endif
int numParts = parts.length();
numParts -= parts.count(".");
@@ -764,11 +740,7 @@ QString pathTruncate(const QString& path, int depth)
return pathTruncate(trunc, depth);
}
-#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
- auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), QString::SkipEmptyParts);
-#else
auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), Qt::SkipEmptyParts);
-#endif
if (parts.startsWith(".") && !path.startsWith(".")) {
parts.removeFirst();
@@ -946,7 +918,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
QDir content = application.path() + "/Contents/";
QDir resources = content.path() + "/Resources/";
QDir binaryDir = content.path() + "/MacOS/";
- QFile info = content.path() + "/Info.plist";
+ QFile info(content.path() + "/Info.plist");
if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) {
qWarning() << "Couldn't create directories within application";
@@ -1291,7 +1263,7 @@ bool clone::operator()(const QString& offset, bool dryRun)
std::error_code err;
// Function that'll do the actual cloneing
- auto cloneFile = [&](QString src_path, QString relative_dst_path) {
+ auto cloneFile = [this, dryRun, dst, &err](QString src_path, QString relative_dst_path) {
if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist))
return;
diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h
index 4aa5596ae..b1108eded 100644
--- a/launcher/FileSystem.h
+++ b/launcher/FileSystem.h
@@ -115,7 +115,7 @@ class copy : public QObject {
m_followSymlinks = follow;
return *this;
}
- copy& matcher(const IPathMatcher* filter)
+ copy& matcher(IPathMatcher::Ptr filter)
{
m_matcher = filter;
return *this;
@@ -147,7 +147,7 @@ class copy : public QObject {
private:
bool m_followSymlinks = true;
- const IPathMatcher* m_matcher = nullptr;
+ IPathMatcher::Ptr m_matcher = nullptr;
bool m_whitelist = false;
bool m_overwrite = false;
QDir m_src;
@@ -209,7 +209,7 @@ class create_link : public QObject {
m_useHardLinks = useHard;
return *this;
}
- create_link& matcher(const IPathMatcher* filter)
+ create_link& matcher(IPathMatcher::Ptr filter)
{
m_matcher = filter;
return *this;
@@ -260,7 +260,7 @@ class create_link : public QObject {
private:
bool m_useHardLinks = false;
- const IPathMatcher* m_matcher = nullptr;
+ IPathMatcher::Ptr m_matcher = nullptr;
bool m_whitelist = false;
bool m_recursive = true;
@@ -491,7 +491,7 @@ class clone : public QObject {
m_src.setPath(src);
m_dst.setPath(dst);
}
- clone& matcher(const IPathMatcher* filter)
+ clone& matcher(IPathMatcher::Ptr filter)
{
m_matcher = filter;
return *this;
@@ -517,7 +517,7 @@ class clone : public QObject {
bool operator()(const QString& offset, bool dryRun = false);
private:
- const IPathMatcher* m_matcher = nullptr;
+ IPathMatcher::Ptr m_matcher = nullptr;
bool m_whitelist = false;
QDir m_src;
QDir m_dst;
diff --git a/launcher/GZip.cpp b/launcher/GZip.cpp
index 1c2539e08..29c71c012 100644
--- a/launcher/GZip.cpp
+++ b/launcher/GZip.cpp
@@ -36,6 +36,8 @@
#include "GZip.h"
#include
#include
+#include
+#include
bool GZip::unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes)
{
@@ -136,3 +138,81 @@ bool GZip::zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes)
}
return true;
}
+
+int inf(QFile* source, std::function handleBlock)
+{
+ constexpr auto CHUNK = 16384;
+ int ret;
+ unsigned have;
+ z_stream strm;
+ memset(&strm, 0, sizeof(strm));
+ char in[CHUNK];
+ unsigned char out[CHUNK];
+
+ ret = inflateInit2(&strm, (16 + MAX_WBITS));
+ if (ret != Z_OK)
+ return ret;
+
+ /* decompress until deflate stream ends or end of file */
+ do {
+ strm.avail_in = source->read(in, CHUNK);
+ if (source->error()) {
+ (void)inflateEnd(&strm);
+ return Z_ERRNO;
+ }
+ if (strm.avail_in == 0)
+ break;
+ strm.next_in = reinterpret_cast(in);
+
+ /* run inflate() on input until output buffer not full */
+ do {
+ strm.avail_out = CHUNK;
+ strm.next_out = out;
+ ret = inflate(&strm, Z_NO_FLUSH);
+ assert(ret != Z_STREAM_ERROR); /* state not clobbered */
+ switch (ret) {
+ case Z_NEED_DICT:
+ ret = Z_DATA_ERROR; /* and fall through */
+ case Z_DATA_ERROR:
+ case Z_MEM_ERROR:
+ (void)inflateEnd(&strm);
+ return ret;
+ }
+ have = CHUNK - strm.avail_out;
+ if (!handleBlock(QByteArray(reinterpret_cast(out), have))) {
+ (void)inflateEnd(&strm);
+ return Z_OK;
+ }
+
+ } while (strm.avail_out == 0);
+
+ /* done when inflate() says it's done */
+ } while (ret != Z_STREAM_END);
+
+ /* clean up and return */
+ (void)inflateEnd(&strm);
+ return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR;
+}
+
+QString zerr(int ret)
+{
+ switch (ret) {
+ case Z_ERRNO:
+ return QObject::tr("error handling file");
+ case Z_STREAM_ERROR:
+ return QObject::tr("invalid compression level");
+ case Z_DATA_ERROR:
+ return QObject::tr("invalid or incomplete deflate data");
+ case Z_MEM_ERROR:
+ return QObject::tr("out of memory");
+ case Z_VERSION_ERROR:
+ return QObject::tr("zlib version mismatch!");
+ }
+ return {};
+}
+
+QString GZip::readGzFileByBlocks(QFile* source, std::function handleBlock)
+{
+ auto ret = inf(source, handleBlock);
+ return zerr(ret);
+}
\ No newline at end of file
diff --git a/launcher/GZip.h b/launcher/GZip.h
index 0bdb70407..b736ca93f 100644
--- a/launcher/GZip.h
+++ b/launcher/GZip.h
@@ -1,8 +1,11 @@
#pragma once
#include
+#include
-class GZip {
- public:
- static bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes);
- static bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes);
-};
+namespace GZip {
+
+bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes);
+bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes);
+QString readGzFileByBlocks(QFile* source, std::function handleBlock);
+
+} // namespace GZip
diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp
index 0220a4144..fb5963532 100644
--- a/launcher/InstanceCopyTask.cpp
+++ b/launcher/InstanceCopyTask.cpp
@@ -43,7 +43,7 @@ void InstanceCopyTask::executeTask()
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] {
if (m_useClone) {
FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath);
- folderClone.matcher(m_matcher.get());
+ folderClone.matcher(m_matcher);
folderClone(true);
setProgress(0, folderClone.totalCloned());
@@ -72,7 +72,7 @@ void InstanceCopyTask::executeTask()
}
FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath);
int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder
- folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get());
+ folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher);
folderLink(true);
setProgress(0, m_progressTotal + folderLink.totalToLink());
@@ -91,7 +91,7 @@ void InstanceCopyTask::executeTask()
QEventLoop loop;
bool got_priv_results = false;
- connect(&folderLink, &FS::create_link::finishedPrivileged, this, [&](bool gotResults) {
+ connect(&folderLink, &FS::create_link::finishedPrivileged, this, [&got_priv_results, &loop](bool gotResults) {
if (!gotResults) {
qDebug() << "Privileged run exited without results!";
}
@@ -127,7 +127,7 @@ void InstanceCopyTask::executeTask()
return !there_were_errors;
}
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
- folderCopy.followSymlinks(false).matcher(m_matcher.get());
+ folderCopy.followSymlinks(false).matcher(m_matcher);
folderCopy(true);
setProgress(0, folderCopy.totalCopied());
diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h
index 0f7f1020d..3aba13e5c 100644
--- a/launcher/InstanceCopyTask.h
+++ b/launcher/InstanceCopyTask.h
@@ -28,7 +28,7 @@ class InstanceCopyTask : public InstanceTask {
InstancePtr m_origInstance;
QFuture m_copyFuture;
QFutureWatcher m_copyFutureWatcher;
- std::unique_ptr m_matcher;
+ IPathMatcher::Ptr m_matcher;
bool m_keepPlaytime;
bool m_useLinks = false;
bool m_useHardLinks = false;
diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp
index 3e7b3142f..bd3514798 100644
--- a/launcher/InstanceCreationTask.cpp
+++ b/launcher/InstanceCreationTask.cpp
@@ -61,6 +61,6 @@ void InstanceCreationTask::executeTask()
return;
}
}
-
- emitSucceeded();
+ if (!m_abort)
+ emitSucceeded();
}
diff --git a/launcher/InstanceDirUpdate.cpp b/launcher/InstanceDirUpdate.cpp
new file mode 100644
index 000000000..8be0dccac
--- /dev/null
+++ b/launcher/InstanceDirUpdate.cpp
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ *
+ * 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 "InstanceDirUpdate.h"
+
+#include
+
+#include "Application.h"
+#include "FileSystem.h"
+
+#include "InstanceList.h"
+#include "ui/dialogs/CustomMessageBox.h"
+
+QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent)
+{
+ if (oldName == newName)
+ return QString();
+
+ QString renamingMode = APPLICATION->settings()->get("InstRenamingMode").toString();
+ if (renamingMode == "MetadataOnly")
+ return QString();
+
+ auto oldRoot = instance->instanceRoot();
+ auto newDirName = FS::DirNameFromString(newName, QFileInfo(oldRoot).dir().absolutePath());
+ auto newRoot = FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newDirName);
+ if (oldRoot == newRoot)
+ return QString();
+ if (oldRoot == FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newName))
+ return QString();
+
+ // Check for conflict
+ if (QDir(newRoot).exists()) {
+ QMessageBox::warning(parent, QObject::tr("Cannot rename instance"),
+ QObject::tr("New instance root (%1) already exists.
Only the metadata will be renamed.").arg(newRoot));
+ return QString();
+ }
+
+ // Ask if we should rename
+ if (renamingMode == "AskEverytime") {
+ auto checkBox = new QCheckBox(QObject::tr("&Remember my choice"), parent);
+ auto dialog =
+ CustomMessageBox::selectable(parent, QObject::tr("Rename instance folder"),
+ QObject::tr("Would you also like to rename the instance folder?\n\n"
+ "Old name: %1\n"
+ "New name: %2")
+ .arg(oldName, newName),
+ QMessageBox::Question, QMessageBox::No | QMessageBox::Yes, QMessageBox::NoButton, checkBox);
+
+ auto res = dialog->exec();
+ if (checkBox->isChecked()) {
+ if (res == QMessageBox::Yes)
+ APPLICATION->settings()->set("InstRenamingMode", "PhysicalDir");
+ else
+ APPLICATION->settings()->set("InstRenamingMode", "MetadataOnly");
+ }
+ if (res == QMessageBox::No)
+ return QString();
+ }
+
+ // Check for linked instances
+ if (!checkLinkedInstances(instance->id(), parent, QObject::tr("Renaming")))
+ return QString();
+
+ // Now we can confirm that a renaming is happening
+ if (!instance->syncInstanceDirName(newRoot)) {
+ QMessageBox::warning(parent, QObject::tr("Cannot rename instance"),
+ QObject::tr("An error occurred when performing the following renaming operation:
"
+ " - Old instance root: %1
"
+ " - New instance root: %2
"
+ "Only the metadata is renamed.")
+ .arg(oldRoot, newRoot));
+ return QString();
+ }
+ return newRoot;
+}
+
+bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb)
+{
+ auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id);
+ if (!linkedInstances.empty()) {
+ auto response = CustomMessageBox::selectable(parent, QObject::tr("There are linked instances"),
+ QObject::tr("The following instance(s) might reference files in this instance:\n\n"
+ "%1\n\n"
+ "%2 it could break the other instance(s), \n\n"
+ "Do you wish to proceed?",
+ nullptr, linkedInstances.count())
+ .arg(linkedInstances.join("\n"))
+ .arg(verb),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
+ if (response != QMessageBox::Yes)
+ return false;
+ }
+ return true;
+}
diff --git a/launcher/InstanceDirUpdate.h b/launcher/InstanceDirUpdate.h
new file mode 100644
index 000000000..b92a59c4c
--- /dev/null
+++ b/launcher/InstanceDirUpdate.h
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ *
+ * 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 "BaseInstance.h"
+
+/// Update instanceRoot to make it sync with name/id; return newRoot if a directory rename happened
+QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent);
+
+/// Check if there are linked instances, and display a warning; return true if the operation should proceed
+bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb);
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
index 57cc77527..e2735385b 100644
--- a/launcher/InstanceImportTask.cpp
+++ b/launcher/InstanceImportTask.cpp
@@ -69,9 +69,10 @@ bool InstanceImportTask::abort()
if (!canAbort())
return false;
- if (task)
- task->abort();
- return Task::abort();
+ bool wasAborted = false;
+ if (m_task)
+ wasAborted = m_task->abort();
+ return wasAborted;
}
void InstanceImportTask::executeTask()
@@ -104,7 +105,7 @@ void InstanceImportTask::downloadFromUrl()
connect(filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::emitFailed);
connect(filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::emitAborted);
- task.reset(filesNetJob);
+ m_task.reset(filesNetJob);
filesNetJob->start();
}
@@ -193,7 +194,7 @@ void InstanceImportTask::processZipPack()
stepProgress(*progressStep);
});
- connect(zipTask.get(), &Task::succeeded, this, &InstanceImportTask::extractFinished);
+ connect(zipTask.get(), &Task::succeeded, this, &InstanceImportTask::extractFinished, Qt::QueuedConnection);
connect(zipTask.get(), &Task::aborted, this, &InstanceImportTask::emitAborted);
connect(zipTask.get(), &Task::failed, this, [this, progressStep](QString reason) {
progressStep->state = TaskStepState::Failed;
@@ -210,12 +211,13 @@ void InstanceImportTask::processZipPack()
progressStep->status = status;
stepProgress(*progressStep);
});
- task.reset(zipTask);
+ m_task.reset(zipTask);
zipTask->start();
}
void InstanceImportTask::extractFinished()
{
+ setAbortable(false);
QDir extractDir(m_stagingPath);
qDebug() << "Fixing permissions for extracted pack files...";
@@ -289,8 +291,11 @@ void InstanceImportTask::processFlame()
inst_creation_task->setGroup(m_instGroup);
inst_creation_task->setConfirmUpdate(shouldConfirmUpdate());
- connect(inst_creation_task.get(), &Task::succeeded, this, [this, inst_creation_task] {
- setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID());
+ auto weak = inst_creation_task.toWeakRef();
+ connect(inst_creation_task.get(), &Task::succeeded, this, [this, weak] {
+ if (auto sp = weak.lock()) {
+ setOverride(sp->shouldOverride(), sp->originalInstanceID());
+ }
emitSucceeded();
});
connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed);
@@ -299,11 +304,12 @@ void InstanceImportTask::processFlame()
connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails);
- connect(this, &Task::aborted, inst_creation_task.get(), &InstanceCreationTask::abort);
- 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);
- inst_creation_task->start();
+ m_task.reset(inst_creation_task);
+ setAbortable(true);
+ m_task->start();
}
void InstanceImportTask::processTechnic()
@@ -350,7 +356,7 @@ void InstanceImportTask::processMultiMC()
void InstanceImportTask::processModrinth()
{
- ModrinthCreationTask* inst_creation_task = nullptr;
+ shared_qobject_ptr inst_creation_task = nullptr;
if (!m_extra_info.isEmpty()) {
auto pack_id_it = m_extra_info.constFind("pack_id");
Q_ASSERT(pack_id_it != m_extra_info.constEnd());
@@ -367,16 +373,16 @@ void InstanceImportTask::processModrinth()
original_instance_id = original_instance_id_it.value();
inst_creation_task =
- new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id);
+ makeShared(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id);
} else {
QString pack_id;
if (!m_sourceUrl.isEmpty()) {
- QRegularExpression regex(R"(data\/([^\/]*)\/versions)");
- pack_id = regex.match(m_sourceUrl.toString()).captured(1);
+ static const QRegularExpression s_regex(R"(data\/([^\/]*)\/versions)");
+ pack_id = s_regex.match(m_sourceUrl.toString()).captured(1);
}
// FIXME: Find a way to get the ID in directly imported ZIPs
- inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id);
+ inst_creation_task = makeShared(m_stagingPath, m_globalSettings, m_parent, pack_id);
}
inst_creation_task->setName(*this);
@@ -384,20 +390,23 @@ void InstanceImportTask::processModrinth()
inst_creation_task->setGroup(m_instGroup);
inst_creation_task->setConfirmUpdate(shouldConfirmUpdate());
- connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] {
- setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID());
+ auto weak = inst_creation_task.toWeakRef();
+ connect(inst_creation_task.get(), &Task::succeeded, this, [this, weak] {
+ if (auto sp = weak.lock()) {
+ setOverride(sp->shouldOverride(), sp->originalInstanceID());
+ }
emitSucceeded();
});
- connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
- connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress);
- connect(inst_creation_task, &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress);
- connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus);
- connect(inst_creation_task, &Task::details, this, &InstanceImportTask::setDetails);
- connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);
+ connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed);
+ connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress);
+ connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress);
+ connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus);
+ connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails);
- connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort);
- connect(inst_creation_task, &Task::aborted, this, &Task::abort);
- connect(inst_creation_task, &Task::abortStatusChanged, this, &Task::setAbortable);
+ connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted);
+ connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable);
- inst_creation_task->start();
+ m_task.reset(inst_creation_task);
+ setAbortable(true);
+ m_task->start();
}
diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h
index cf86af4ea..8884e0801 100644
--- a/launcher/InstanceImportTask.h
+++ b/launcher/InstanceImportTask.h
@@ -40,16 +40,13 @@
#include
#include "InstanceTask.h"
-#include
-#include
-
class QuaZip;
class InstanceImportTask : public InstanceTask {
Q_OBJECT
public:
explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap&& extra_info = {});
-
+ virtual ~InstanceImportTask() = default;
bool abort() override;
protected:
@@ -70,7 +67,7 @@ class InstanceImportTask : public InstanceTask {
private: /* data */
QUrl m_sourceUrl;
QString m_archivePath;
- Task::Ptr task;
+ Task::Ptr m_task;
enum class ModpackType {
Unknown,
MultiMC,
diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp
index e1fa755dd..89e7dc04d 100644
--- a/launcher/InstanceList.cpp
+++ b/launcher/InstanceList.cpp
@@ -428,7 +428,7 @@ static QMap getIdMapping(const QList&
QList InstanceList::discoverInstances()
{
- qDebug() << "Discovering instances in" << m_instDir;
+ qInfo() << "Discovering instances in" << m_instDir;
QList out;
QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks);
while (iter.hasNext()) {
@@ -447,13 +447,9 @@ QList InstanceList::discoverInstances()
}
auto id = dirInfo.fileName();
out.append(id);
- qDebug() << "Found instance ID" << id;
+ qInfo() << "Found instance ID" << id;
}
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
instanceSet = QSet(out.begin(), out.end());
-#else
- instanceSet = out.toSet();
-#endif
m_instancesProbed = true;
return out;
}
@@ -468,7 +464,7 @@ InstanceList::InstListError InstanceList::loadList()
if (existingIds.contains(id)) {
auto instPair = existingIds[id];
existingIds.remove(id);
- qDebug() << "Should keep and soft-reload" << id;
+ qInfo() << "Should keep and soft-reload" << id;
} else {
InstancePtr instPtr = loadInstance(id);
if (instPtr) {
@@ -487,7 +483,7 @@ InstanceList::InstListError InstanceList::loadList()
int front_bookmark = -1;
int back_bookmark = -1;
int currentItem = -1;
- auto removeNow = [&]() {
+ auto removeNow = [this, &front_bookmark, &back_bookmark, ¤tItem]() {
beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark);
m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1);
endRemoveRows();
diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h
index 174041f89..076341b0b 100644
--- a/launcher/InstancePageProvider.h
+++ b/launcher/InstancePageProvider.h
@@ -43,11 +43,8 @@ class InstancePageProvider : protected QObject, public BasePageProvider {
values.append(new ServersPage(onesix));
// values.append(new GameOptionsPage(onesix.get()));
values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots")));
- values.append(new InstanceSettingsPage(onesix.get()));
- auto logMatcher = inst->getLogFileMatcher();
- if (logMatcher) {
- values.append(new OtherLogsPage(inst->getLogFileRoot(), logMatcher));
- }
+ values.append(new InstanceSettingsPage(onesix));
+ values.append(new OtherLogsPage(inst));
return values;
}
diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp
index 3cbf9f9d5..b71000054 100644
--- a/launcher/JavaCommon.cpp
+++ b/launcher/JavaCommon.cpp
@@ -41,7 +41,9 @@
bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent)
{
- if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(QRegularExpression("-Xm[sx]")) || jvmargs.contains("-XX-MaxHeapSize") ||
+ static const QRegularExpression s_memRegex("-Xm[sx]");
+ static const QRegularExpression s_versionRegex("-version:.*");
+ if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(s_memRegex) || jvmargs.contains("-XX-MaxHeapSize") ||
jvmargs.contains("-XX:InitialHeapSize")) {
auto warnStr = QObject::tr(
"You tried to manually set a JVM memory option (using \"-XX:PermSize\", \"-XX-MaxHeapSize\", \"-XX:InitialHeapSize\", \"-Xmx\" "
@@ -52,7 +54,7 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent)
return false;
}
// block lunacy with passing required version to the JVM
- if (jvmargs.contains(QRegularExpression("-version:.*"))) {
+ if (jvmargs.contains(s_versionRegex)) {
auto warnStr = QObject::tr(
"You tried to pass required Java version argument to the JVM (using \"-version:xxx\"). This is not safe and will not be "
"allowed.\n"
@@ -116,7 +118,7 @@ void JavaCommon::TestCheck::run()
emit finished();
return;
}
- checker.reset(new JavaChecker(m_path, "", 0, 0, 0, 0, this));
+ checker.reset(new JavaChecker(m_path, "", 0, 0, 0, 0));
connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinished);
checker->start();
}
@@ -128,7 +130,7 @@ void JavaCommon::TestCheck::checkFinished(const JavaChecker::Result& result)
emit finished();
return;
}
- checker.reset(new JavaChecker(m_path, m_args, m_maxMem, m_maxMem, result.javaVersion.requiresPermGen() ? m_permGen : 0, 0, this));
+ checker.reset(new JavaChecker(m_path, m_args, m_maxMem, m_maxMem, result.javaVersion.requiresPermGen() ? m_permGen : 0, 0));
connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinishedWithArgs);
checker->start();
}
diff --git a/launcher/JavaCommon.h b/launcher/JavaCommon.h
index a21b5a494..0e4aa2b0a 100644
--- a/launcher/JavaCommon.h
+++ b/launcher/JavaCommon.h
@@ -24,7 +24,7 @@ class TestCheck : public QObject {
TestCheck(QWidget* parent, QString path, QString args, int minMem, int maxMem, int permGen)
: m_parent(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen)
{}
- virtual ~TestCheck() {};
+ virtual ~TestCheck() = default;
void run();
diff --git a/launcher/Json.h b/launcher/Json.h
index 28891f398..c13be6470 100644
--- a/launcher/Json.h
+++ b/launcher/Json.h
@@ -188,10 +188,10 @@ T ensureIsType(const QJsonObject& parent, const QString& key, const T default_ =
}
template
-QVector requireIsArrayOf(const QJsonDocument& doc)
+QList requireIsArrayOf(const QJsonDocument& doc)
{
const QJsonArray array = requireArray(doc);
- QVector out;
+ QList out;
for (const QJsonValue val : array) {
out.append(requireIsType(val, "Document"));
}
@@ -199,10 +199,10 @@ QVector requireIsArrayOf(const QJsonDocument& doc)
}
template
-QVector ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value")
+QList ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value")
{
const QJsonArray array = ensureIsType(value, QJsonArray(), what);
- QVector out;
+ QList out;
for (const QJsonValue val : array) {
out.append(requireIsType(val, what));
}
@@ -210,7 +210,7 @@ QVector ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value
}
template
-QVector ensureIsArrayOf(const QJsonValue& value, const QVector default_, const QString& what = "Value")
+QList ensureIsArrayOf(const QJsonValue& value, const QList default_, const QString& what = "Value")
{
if (value.isUndefined()) {
return default_;
@@ -220,7 +220,7 @@ QVector ensureIsArrayOf(const QJsonValue& value, const QVector default_, c
/// @throw JsonException
template
-QVector requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__")
+QList requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__")
{
const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
if (!parent.contains(key)) {
@@ -230,10 +230,10 @@ QVector requireIsArrayOf(const QJsonObject& parent, const QString& key, const
}
template
-QVector ensureIsArrayOf(const QJsonObject& parent,
- const QString& key,
- const QVector& default_ = QVector(),
- const QString& what = "__placeholder__")
+QList ensureIsArrayOf(const QJsonObject& parent,
+ const QString& key,
+ const QList& default_ = QList(),
+ const QString& what = "__placeholder__")
{
const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
if (!parent.contains(key)) {
diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp
index 687da1322..b1a956b49 100644
--- a/launcher/LaunchController.cpp
+++ b/launcher/LaunchController.cpp
@@ -43,6 +43,7 @@
#include "ui/InstanceWindow.h"
#include "ui/MainWindow.h"
#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/dialogs/MSALoginDialog.h"
#include "ui/dialogs/ProfileSelectDialog.h"
#include "ui/dialogs/ProfileSetupDialog.h"
#include "ui/dialogs/ProgressDialog.h"
@@ -61,7 +62,7 @@
#include "launch/steps/TextPrint.h"
#include "tasks/Task.h"
-LaunchController::LaunchController(QObject* parent) : Task(parent) {}
+LaunchController::LaunchController() : Task() {}
void LaunchController::executeTask()
{
@@ -181,7 +182,8 @@ void LaunchController::login()
auto name = askOfflineName("Player", m_demo, ok);
if (ok) {
m_session = std::make_shared();
- m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(QRegularExpression("[{}-]")));
+ static const QRegularExpression s_removeChars("[{}-]");
+ m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(s_removeChars));
launchInstance();
return;
}
@@ -199,8 +201,7 @@ void LaunchController::login()
m_accountToUse->shouldRefresh()) {
// Force account refresh on the account used to launch the instance updating the AccountState
// only on first try and if it is not meant to be offline
- auto accounts = APPLICATION->accounts();
- accounts->requestRefresh(m_accountToUse->internalId());
+ m_accountToUse->refresh();
}
while (tryagain) {
if (tries > 0 && tries % 3 == 0) {
@@ -219,13 +220,34 @@ void LaunchController::login()
m_session->demo = m_demo;
m_accountToUse->fillSession(m_session);
- // Launch immediately in true offline mode
- if (m_accountToUse->accountType() == AccountType::Offline) {
- launchInstance();
+ MinecraftAccountPtr accountToCheck;
+
+ if (m_accountToUse->ownsMinecraft())
+ accountToCheck = m_accountToUse;
+ else if (const MinecraftAccountPtr defaultAccount = APPLICATION->accounts()->defaultAccount();
+ defaultAccount != nullptr && defaultAccount->ownsMinecraft()) {
+ accountToCheck = defaultAccount;
+ } else {
+ for (int i = 0; i < APPLICATION->accounts()->count(); i++) {
+ MinecraftAccountPtr account = APPLICATION->accounts()->at(i);
+ if (account->ownsMinecraft())
+ accountToCheck = account;
+ }
+ }
+
+ if (accountToCheck == nullptr) {
+ if (!m_session->demo)
+ m_session->demo = askPlayDemo();
+
+ if (m_session->demo)
+ launchInstance();
+ else
+ emitFailed(tr("Launch cancelled - account does not own Minecraft."));
+
return;
}
- switch (m_accountToUse->accountState()) {
+ switch (accountToCheck->accountState()) {
case AccountState::Offline: {
m_session->wants_online = false;
}
@@ -234,46 +256,41 @@ void LaunchController::login()
if (!m_session->wants_online) {
// we ask the user for a player name
bool ok = false;
- auto name = askOfflineName(m_session->player_name, m_session->demo, ok);
- if (!ok) {
- tryagain = false;
- break;
+ QString name;
+ if (m_offlineName.isEmpty()) {
+ name = askOfflineName(m_session->player_name, m_session->demo, ok);
+ if (!ok) {
+ tryagain = false;
+ break;
+ }
+ } else {
+ name = m_offlineName;
}
m_session->MakeOffline(name);
// offline flavored game from here :3
- }
- if (m_accountToUse->ownsMinecraft()) {
- if (!m_accountToUse->hasProfile()) {
- // Now handle setting up a profile name here...
- ProfileSetupDialog dialog(m_accountToUse, m_parentWidget);
- if (dialog.exec() == QDialog::Accepted) {
- tryagain = true;
- continue;
- } else {
- emitFailed(tr("Received undetermined session status during login."));
- return;
- }
- }
- // we own Minecraft, there is a profile, it's all ready to go!
- launchInstance();
- return;
- } else {
- // play demo ?
- if (!m_session->demo) {
- m_session->demo = askPlayDemo();
- }
- if (m_session->demo) { // play demo here
- launchInstance();
+ } else if (m_accountToUse == accountToCheck && !m_accountToUse->hasProfile()) {
+ // Now handle setting up a profile name here...
+ ProfileSetupDialog dialog(m_accountToUse, m_parentWidget);
+ if (dialog.exec() == QDialog::Accepted) {
+ tryagain = true;
+ continue;
} else {
- emitFailed(tr("Launch cancelled - account does not own Minecraft."));
+ emitFailed(tr("Received undetermined session status during login."));
+ return;
}
}
+
+ if (m_accountToUse->accountType() == AccountType::Offline)
+ m_session->wants_online = false;
+
+ // we own Minecraft, there is a profile, it's all ready to go!
+ launchInstance();
return;
}
case AccountState::Errored:
// This means some sort of soft error that we can fix with a refresh ... so let's refresh.
case AccountState::Unchecked: {
- m_accountToUse->refresh();
+ accountToCheck->refresh();
}
/* fallthrough */
case AccountState::Working: {
@@ -282,19 +299,19 @@ void LaunchController::login()
if (m_online) {
progDialog.setSkipButton(true, tr("Play Offline"));
}
- auto task = m_accountToUse->currentTask();
+ auto task = accountToCheck->currentTask();
progDialog.execWithTask(task.get());
continue;
}
case AccountState::Expired: {
- auto errorString = tr("The account has expired and needs to be logged into manually again.");
- QMessageBox::warning(m_parentWidget, tr("Account refresh failed"), errorString, QMessageBox::StandardButton::Ok,
- QMessageBox::StandardButton::Ok);
- emitFailed(errorString);
+ if (reauthenticateAccount(accountToCheck))
+ continue;
return;
}
case AccountState::Disabled: {
- auto errorString = tr("The launcher's client identification has changed. Please remove this account and add it again.");
+ auto errorString = tr("The launcher's client identification has changed. Please remove '%1' and try again.")
+ .arg(accountToCheck->profileName());
+
QMessageBox::warning(m_parentWidget, tr("Client identification changed"), errorString, QMessageBox::StandardButton::Ok,
QMessageBox::StandardButton::Ok);
emitFailed(errorString);
@@ -302,8 +319,9 @@ void LaunchController::login()
}
case AccountState::Gone: {
auto errorString =
- tr("The account no longer exists on the servers. It may have been migrated, in which case please add the new account "
- "you migrated this one to.");
+ tr("'%1' no longer exists on the servers. It may have been migrated, in which case please add the new account "
+ "you migrated this one to.")
+ .arg(accountToCheck->profileName());
QMessageBox::warning(m_parentWidget, tr("Account gone"), errorString, QMessageBox::StandardButton::Ok,
QMessageBox::StandardButton::Ok);
emitFailed(errorString);
@@ -314,6 +332,38 @@ void LaunchController::login()
emitFailed(tr("Failed to launch."));
}
+bool LaunchController::reauthenticateAccount(MinecraftAccountPtr account)
+{
+ auto button = QMessageBox::warning(
+ m_parentWidget, tr("Account refresh failed"),
+ tr("'%1' has expired and needs to be reauthenticated. Do you want to reauthenticate this account?").arg(account->profileName()),
+ QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::Yes);
+ if (button == QMessageBox::StandardButton::Yes) {
+ auto accounts = APPLICATION->accounts();
+ bool isDefault = accounts->defaultAccount() == account;
+ accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId())));
+ if (account->accountType() == AccountType::MSA) {
+ auto newAccount = MSALoginDialog::newAccount(m_parentWidget);
+
+ if (newAccount != nullptr) {
+ accounts->addAccount(newAccount);
+
+ if (isDefault)
+ accounts->setDefaultAccount(newAccount);
+
+ if (m_accountToUse == account) {
+ m_accountToUse = nullptr;
+ decideAccount();
+ }
+ return true;
+ }
+ }
+ }
+
+ emitFailed(tr("The account has expired and needs to be reauthenticated"));
+ return false;
+}
+
void LaunchController::launchInstance()
{
Q_ASSERT_X(m_instance != NULL, "launchInstance", "instance is NULL");
diff --git a/launcher/LaunchController.h b/launcher/LaunchController.h
index 6e2a94258..af57994f5 100644
--- a/launcher/LaunchController.h
+++ b/launcher/LaunchController.h
@@ -47,7 +47,7 @@ class LaunchController : public Task {
public:
void executeTask() override;
- LaunchController(QObject* parent = nullptr);
+ LaunchController();
virtual ~LaunchController() = default;
void setInstance(InstancePtr instance) { m_instance = instance; }
@@ -56,6 +56,8 @@ class LaunchController : public Task {
void setOnline(bool online) { m_online = online; }
+ void setOfflineName(const QString& offlineName) { m_offlineName = offlineName; }
+
void setDemo(bool demo) { m_demo = demo; }
void setProfiler(BaseProfilerFactory* profiler) { m_profiler = profiler; }
@@ -76,6 +78,7 @@ class LaunchController : public Task {
void decideAccount();
bool askPlayDemo();
QString askOfflineName(QString playerName, bool demo, bool& ok);
+ bool reauthenticateAccount(MinecraftAccountPtr account);
private slots:
void readyForLaunch();
@@ -87,6 +90,7 @@ class LaunchController : public Task {
private:
BaseProfilerFactory* m_profiler = nullptr;
bool m_online = true;
+ QString m_offlineName;
bool m_demo = false;
InstancePtr m_instance;
QWidget* m_parentWidget = nullptr;
diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp
index dcf3d566f..0b1a2b39e 100644
--- a/launcher/MMCZip.cpp
+++ b/launcher/MMCZip.cpp
@@ -378,7 +378,7 @@ std::optional extractDir(QString fileCompressed, QString dir)
if (fileInfo.size() == 22) {
return QStringList();
}
- qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();
+ qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError();
;
return std::nullopt;
}
@@ -395,7 +395,7 @@ std::optional extractDir(QString fileCompressed, QString subdir, QS
if (fileInfo.size() == 22) {
return QStringList();
}
- qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();
+ qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError();
;
return std::nullopt;
}
@@ -412,13 +412,13 @@ bool extractFile(QString fileCompressed, QString file, QString target)
if (fileInfo.size() == 22) {
return true;
}
- qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();
+ qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError();
return false;
}
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;
}
@@ -577,7 +577,7 @@ auto ExtractZipTask::extractZip() -> ZipResult
auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size()));
auto original_name = relative_file_name;
- setStatus("Unziping: " + relative_file_name);
+ setStatus("Unpacking: " + relative_file_name);
// Fix subdirs/files ending with a / getting transformed into absolute paths
if (relative_file_name.startsWith('/'))
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/MessageLevel.cpp b/launcher/MessageLevel.cpp
index 116e70c4b..2bd6ecc00 100644
--- a/launcher/MessageLevel.cpp
+++ b/launcher/MessageLevel.cpp
@@ -2,19 +2,22 @@
MessageLevel::Enum MessageLevel::getLevel(const QString& levelName)
{
- if (levelName == "Launcher")
+ QString name = levelName.toUpper();
+ if (name == "LAUNCHER")
return MessageLevel::Launcher;
- else if (levelName == "Debug")
+ else if (name == "TRACE")
+ return MessageLevel::Trace;
+ else if (name == "DEBUG")
return MessageLevel::Debug;
- else if (levelName == "Info")
+ else if (name == "INFO")
return MessageLevel::Info;
- else if (levelName == "Message")
+ else if (name == "MESSAGE")
return MessageLevel::Message;
- else if (levelName == "Warning")
+ else if (name == "WARNING" || name == "WARN")
return MessageLevel::Warning;
- else if (levelName == "Error")
+ else if (name == "ERROR")
return MessageLevel::Error;
- else if (levelName == "Fatal")
+ else if (name == "FATAL")
return MessageLevel::Fatal;
// Skip PrePost, it's not exposed to !![]!
// Also skip StdErr and StdOut
diff --git a/launcher/MessageLevel.h b/launcher/MessageLevel.h
index fd12583f2..321af9d92 100644
--- a/launcher/MessageLevel.h
+++ b/launcher/MessageLevel.h
@@ -12,6 +12,7 @@ enum Enum {
StdOut, /**< Undetermined stderr messages */
StdErr, /**< Undetermined stdout messages */
Launcher, /**< Launcher Messages */
+ Trace, /**< Trace Messages */
Debug, /**< Debug Messages */
Info, /**< Info Messages */
Message, /**< Standard Messages */
diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h
index 3d01c9d33..e603b1634 100644
--- a/launcher/NullInstance.h
+++ b/launcher/NullInstance.h
@@ -57,8 +57,7 @@ class NullInstance : public BaseInstance {
QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); }
QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); }
QMap getVariables() override { return QMap(); }
- IPathMatcher::Ptr getLogFileMatcher() override { return nullptr; }
- QString getLogFileRoot() override { return instanceRoot(); }
+ QStringList getLogFileSearchPaths() override { return {}; }
QString typeName() const override { return "Null"; }
bool canExport() const override { return false; }
bool canEdit() const override { return false; }
diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h
index a1c64b433..88c17c0b2 100644
--- a/launcher/QObjectPtr.h
+++ b/launcher/QObjectPtr.h
@@ -33,7 +33,7 @@ class shared_qobject_ptr : public QSharedPointer {
{}
void reset() { QSharedPointer::reset(); }
- void reset(T*&& other)
+ void reset(T* other)
{
shared_qobject_ptr t(other);
this->swap(t);
diff --git a/launcher/RecursiveFileSystemWatcher.cpp b/launcher/RecursiveFileSystemWatcher.cpp
index 8b28a03f1..5cb3cd0be 100644
--- a/launcher/RecursiveFileSystemWatcher.cpp
+++ b/launcher/RecursiveFileSystemWatcher.cpp
@@ -1,7 +1,6 @@
#include "RecursiveFileSystemWatcher.h"
#include
-#include
RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject* parent) : QObject(parent), m_watcher(new QFileSystemWatcher(this))
{
diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp
index 6f5b9a189..0fe082ac4 100644
--- a/launcher/ResourceDownloadTask.cpp
+++ b/launcher/ResourceDownloadTask.cpp
@@ -35,9 +35,9 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
QString custom_target_folder)
: m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs), m_custom_target_folder(custom_target_folder)
{
- if (auto model = dynamic_cast(m_pack_model.get()); model && is_indexed) {
- m_update_task.reset(new LocalModUpdateTask(model->indexDir(), *m_pack, m_pack_version));
- connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ResourceDownloadTask::hasOldResource);
+ if (is_indexed) {
+ m_update_task.reset(new LocalResourceUpdateTask(m_pack_model->indexDir(), *m_pack, m_pack_version));
+ connect(m_update_task.get(), &LocalResourceUpdateTask::hasOldResource, this, &ResourceDownloadTask::hasOldResource);
addTask(m_update_task);
}
@@ -91,12 +91,8 @@ void ResourceDownloadTask::downloadSucceeded()
m_filesNetJob.reset();
auto name = std::get<0>(to_delete);
auto filename = std::get<1>(to_delete);
- if (!name.isEmpty() && filename != m_pack_version.fileName) {
- if (auto model = dynamic_cast(m_pack_model.get()); model)
- model->uninstallMod(filename, true);
- else
- m_pack_model->uninstallResource(filename);
- }
+ if (!name.isEmpty() && filename != m_pack_version.fileName)
+ m_pack_model->uninstallResource(filename, true);
}
void ResourceDownloadTask::downloadFailed(QString reason)
diff --git a/launcher/ResourceDownloadTask.h b/launcher/ResourceDownloadTask.h
index f686e819a..a10e0ac2c 100644
--- a/launcher/ResourceDownloadTask.h
+++ b/launcher/ResourceDownloadTask.h
@@ -22,7 +22,7 @@
#include "net/NetJob.h"
#include "tasks/SequentialTask.h"
-#include "minecraft/mod/tasks/LocalModUpdateTask.h"
+#include "minecraft/mod/tasks/LocalResourceUpdateTask.h"
#include "modplatform/ModIndex.h"
class ResourceFolderModel;
@@ -50,7 +50,7 @@ class ResourceDownloadTask : public SequentialTask {
QString m_custom_target_folder;
NetJob::Ptr m_filesNetJob;
- LocalModUpdateTask::Ptr m_update_task;
+ LocalResourceUpdateTask::Ptr m_update_task;
void downloadProgressChanged(qint64 current, qint64 total);
void downloadFailed(QString reason);
diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp
index edda9f247..b9e875482 100644
--- a/launcher/StringUtils.cpp
+++ b/launcher/StringUtils.cpp
@@ -53,7 +53,7 @@ static inline QChar getNextChar(const QString& s, int location)
int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs)
{
int l1 = 0, l2 = 0;
- while (l1 <= s1.count() && l2 <= s2.count()) {
+ while (l1 <= s1.size() && l2 <= s2.size()) {
// skip spaces, tabs and 0's
QChar c1 = getNextChar(s1, l1);
while (c1.isSpace())
@@ -213,11 +213,10 @@ QPair StringUtils::splitFirst(const QString& s, const QRegular
return qMakePair(left, right);
}
-static const QRegularExpression ulMatcher("<\\s*/\\s*ul\\s*>");
-
QString StringUtils::htmlListPatch(QString htmlStr)
{
- int pos = htmlStr.indexOf(ulMatcher);
+ static const QRegularExpression s_ulMatcher("<\\s*/\\s*ul\\s*>");
+ int pos = htmlStr.indexOf(s_ulMatcher);
int imgPos;
while (pos != -1) {
pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the tag. Add one for zeroeth index
@@ -230,7 +229,7 @@ QString StringUtils::htmlListPatch(QString htmlStr)
if (textBetween.isEmpty())
htmlStr.insert(pos, "
");
- pos = htmlStr.indexOf(ulMatcher, pos);
+ pos = htmlStr.indexOf(s_ulMatcher, pos);
}
return htmlStr;
}
\ No newline at end of file
diff --git a/launcher/SysInfo.cpp b/launcher/SysInfo.cpp
index 0dfa74de7..cfcf63805 100644
--- a/launcher/SysInfo.cpp
+++ b/launcher/SysInfo.cpp
@@ -81,9 +81,9 @@ QString getSupportedJavaArchitecture()
if (arch == "arm64")
return "mac-os-arm64";
if (arch.contains("64"))
- return "mac-os-64";
+ return "mac-os-x64";
if (arch.contains("86"))
- return "mac-os-86";
+ return "mac-os-x86";
// Unknown, maybe something new, appending arch
return "mac-os-" + arch;
} else if (sys == "linux") {
diff --git a/launcher/Version.cpp b/launcher/Version.cpp
index 2edb17e72..bffe5d58a 100644
--- a/launcher/Version.cpp
+++ b/launcher/Version.cpp
@@ -1,7 +1,6 @@
#include "Version.h"
#include
-#include
#include
#include
@@ -79,7 +78,7 @@ void Version::parse()
if (m_string.isEmpty())
return;
- auto classChange = [&](QChar lastChar, QChar currentChar) {
+ auto classChange = [¤tSection](QChar lastChar, QChar currentChar) {
if (lastChar.isNull())
return false;
if (lastChar.isDigit() != currentChar.isDigit())
diff --git a/launcher/Version.h b/launcher/Version.h
index b06e256aa..12e7f0832 100644
--- a/launcher/Version.h
+++ b/launcher/Version.h
@@ -72,22 +72,14 @@ class Version {
}
}
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
auto numPart = QStringView{ m_fullString }.left(cutoff);
-#else
- auto numPart = m_fullString.leftRef(cutoff);
-#endif
if (!numPart.isEmpty()) {
m_isNull = false;
m_numPart = numPart.toInt();
}
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
auto stringPart = QStringView{ m_fullString }.mid(cutoff);
-#else
- auto stringPart = m_fullString.midRef(cutoff);
-#endif
if (!stringPart.isEmpty()) {
m_isNull = false;
diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp
index 12a82f73d..165dd4cb7 100644
--- a/launcher/VersionProxyModel.cpp
+++ b/launcher/VersionProxyModel.cpp
@@ -193,8 +193,8 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const
if (value.toBool()) {
return tr("Recommended");
} else if (hasLatest) {
- auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
- if (value.toBool()) {
+ auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
+ if (latest.toBool()) {
return tr("Latest");
}
}
@@ -203,33 +203,27 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const
}
}
case Qt::DecorationRole: {
- switch (column) {
- case Name: {
- if (hasRecommended) {
- auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
- if (recommenced.toBool()) {
- return APPLICATION->getThemedIcon("star");
- } else if (hasLatest) {
- auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
- if (latest.toBool()) {
- return APPLICATION->getThemedIcon("bug");
- }
- }
- QPixmap pixmap;
- QPixmapCache::find("placeholder", &pixmap);
- if (!pixmap) {
- QPixmap px(16, 16);
- px.fill(Qt::transparent);
- QPixmapCache::insert("placeholder", px);
- return px;
- }
- return pixmap;
+ if (column == Name && hasRecommended) {
+ auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
+ if (recommenced.toBool()) {
+ return APPLICATION->getThemedIcon("star");
+ } else if (hasLatest) {
+ auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
+ if (latest.toBool()) {
+ return APPLICATION->getThemedIcon("bug");
}
}
- default: {
- return QVariant();
+ QPixmap pixmap;
+ QPixmapCache::find("placeholder", &pixmap);
+ if (!pixmap) {
+ QPixmap px(16, 16);
+ px.fill(Qt::transparent);
+ QPixmapCache::insert("placeholder", px);
+ return px;
}
+ return pixmap;
}
+ return QVariant();
}
default: {
if (roles.contains((BaseVersionList::ModelRoles)role)) {
@@ -301,7 +295,6 @@ void VersionProxyModel::sourceDataChanged(const QModelIndex& source_top_left, co
void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw)
{
auto replacing = dynamic_cast(replacingRaw);
- beginResetModel();
m_columns.clear();
if (!replacing) {
@@ -348,8 +341,6 @@ void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw)
hasLatest = true;
}
filterModel->setSourceModel(replacing);
-
- endResetModel();
}
QModelIndex VersionProxyModel::getRecommended() const
diff --git a/launcher/console/Console.h b/launcher/console/Console.h
new file mode 100644
index 000000000..7aaf83dcc
--- /dev/null
+++ b/launcher/console/Console.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include
+
+#include
+#if defined Q_OS_WIN32
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#include
+#else
+#include
+#include
+#endif
+
+namespace console {
+
+inline bool isConsole()
+{
+#if defined Q_OS_WIN32
+ DWORD procIDs[2];
+ DWORD maxCount = 2;
+ DWORD result = GetConsoleProcessList((LPDWORD)procIDs, maxCount);
+ return result > 1;
+#else
+ if (isatty(fileno(stdout))) {
+ return true;
+ }
+ return false;
+#endif
+}
+
+} // namespace console
diff --git a/launcher/WindowsConsole.cpp b/launcher/console/WindowsConsole.cpp
similarity index 78%
rename from launcher/WindowsConsole.cpp
rename to launcher/console/WindowsConsole.cpp
index 83cad5afa..4a0eb3d3d 100644
--- a/launcher/WindowsConsole.cpp
+++ b/launcher/console/WindowsConsole.cpp
@@ -16,13 +16,18 @@
*
*/
+#include "WindowsConsole.h"
+#include
+
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
+#include
+
#include
#include
#include
-#include
+#include
#include
void RedirectHandle(DWORD handle, FILE* stream, const char* mode)
@@ -126,3 +131,29 @@ bool AttachWindowsConsole()
return false;
}
+
+std::error_code EnableAnsiSupport()
+{
+ // ref: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew
+ // Using `CreateFileW("CONOUT$", ...)` to retrieve the console handle works correctly even if STDOUT and/or STDERR are redirected
+ HANDLE console_handle = CreateFileW(L"CONOUT$", FILE_GENERIC_READ | FILE_GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
+ if (console_handle == INVALID_HANDLE_VALUE) {
+ return std::error_code(GetLastError(), std::system_category());
+ }
+
+ // ref: https://docs.microsoft.com/en-us/windows/console/getconsolemode
+ DWORD console_mode;
+ if (0 == GetConsoleMode(console_handle, &console_mode)) {
+ return std::error_code(GetLastError(), std::system_category());
+ }
+
+ // VT processing not already enabled?
+ if ((console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0) {
+ // https://docs.microsoft.com/en-us/windows/console/setconsolemode
+ if (0 == SetConsoleMode(console_handle, console_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) {
+ return std::error_code(GetLastError(), std::system_category());
+ }
+ }
+
+ return {};
+}
diff --git a/launcher/WindowsConsole.h b/launcher/console/WindowsConsole.h
similarity index 93%
rename from launcher/WindowsConsole.h
rename to launcher/console/WindowsConsole.h
index ab53864b4..4c1f3ee28 100644
--- a/launcher/WindowsConsole.h
+++ b/launcher/console/WindowsConsole.h
@@ -21,5 +21,8 @@
#pragma once
+#include
+
void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr);
bool AttachWindowsConsole();
+std::error_code EnableAnsiSupport();
diff --git a/launcher/filelink/FileLink.cpp b/launcher/filelink/FileLink.cpp
index bdf173ebc..1494fa8cc 100644
--- a/launcher/filelink/FileLink.cpp
+++ b/launcher/filelink/FileLink.cpp
@@ -37,27 +37,14 @@
#include
#if defined Q_OS_WIN32
-#include "WindowsConsole.h"
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#include "console/WindowsConsole.h"
#endif
-// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
-
-#ifdef __APPLE__
-#include // for deployment target to support pre-catalina targets without std::fs
-#endif // __APPLE__
-
-#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
-#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
-#define GHC_USE_STD_FS
#include
namespace fs = std::filesystem;
-#endif // MacOS min version check
-#endif // Other OSes version check
-
-#ifndef GHC_USE_STD_FS
-#include
-namespace fs = ghc::filesystem;
-#endif
FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this))
{
@@ -104,11 +91,11 @@ void FileLinkApp::joinServer(QString server)
in.setDevice(&socket);
- connect(&socket, &QLocalSocket::connected, this, [&]() { qDebug() << "connected to server"; });
+ connect(&socket, &QLocalSocket::connected, this, []() { qDebug() << "connected to server"; });
connect(&socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs);
- connect(&socket, &QLocalSocket::errorOccurred, this, [&](QLocalSocket::LocalSocketError socketError) {
+ connect(&socket, &QLocalSocket::errorOccurred, this, [this](QLocalSocket::LocalSocketError socketError) {
m_status = Failed;
switch (socketError) {
case QLocalSocket::ServerNotFoundError:
@@ -132,7 +119,7 @@ void FileLinkApp::joinServer(QString server)
}
});
- connect(&socket, &QLocalSocket::disconnected, this, [&]() {
+ connect(&socket, &QLocalSocket::disconnected, this, [this]() {
qDebug() << "disconnected from server, should exit";
m_status = Succeeded;
exit();
diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp
index e4157ea2d..8a2a482e1 100644
--- a/launcher/icons/IconList.cpp
+++ b/launcher/icons/IconList.cpp
@@ -37,7 +37,6 @@
#include "IconList.h"
#include
#include
-#include
#include
#include
#include
@@ -47,24 +46,24 @@
#define MAX_SIZE 1024
-IconList::IconList(const QStringList& builtinPaths, QString path, QObject* parent) : QAbstractListModel(parent)
+IconList::IconList(const QStringList& builtinPaths, const QString& path, QObject* parent) : QAbstractListModel(parent)
{
QSet builtinNames;
// add builtin icons
- for (auto& builtinPath : builtinPaths) {
- QDir instance_icons(builtinPath);
- auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name);
- for (auto file_info : file_info_list) {
- builtinNames.insert(file_info.completeBaseName());
+ for (const auto& builtinPath : builtinPaths) {
+ QDir instanceIcons(builtinPath);
+ auto fileInfoList = instanceIcons.entryInfoList(QDir::Files, QDir::Name);
+ for (const auto& fileInfo : fileInfoList) {
+ builtinNames.insert(fileInfo.baseName());
}
}
- for (auto& builtinName : builtinNames) {
+ for (const auto& builtinName : builtinNames) {
addThemeIcon(builtinName);
}
m_watcher.reset(new QFileSystemWatcher());
- is_watching = false;
+ m_isWatching = false;
connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged);
connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged);
@@ -77,91 +76,129 @@ IconList::IconList(const QStringList& builtinPaths, QString path, QObject* paren
void IconList::sortIconList()
{
qDebug() << "Sorting icon list...";
- std::sort(icons.begin(), icons.end(), [](const MMCIcon& a, const MMCIcon& b) { return a.m_key.localeAwareCompare(b.m_key) < 0; });
+ std::sort(m_icons.begin(), m_icons.end(), [](const MMCIcon& a, const MMCIcon& b) {
+ bool aIsSubdir = a.m_key.contains(QDir::separator());
+ bool bIsSubdir = b.m_key.contains(QDir::separator());
+ if (aIsSubdir != bIsSubdir) {
+ return !aIsSubdir; // root-level icons come first
+ }
+ return a.m_key.localeAwareCompare(b.m_key) < 0;
+ });
reindex();
}
+// Helper function to add directories recursively
+bool IconList::addPathRecursively(const QString& path)
+{
+ QDir dir(path);
+ if (!dir.exists())
+ return false;
+
+ // Add the directory itself
+ bool watching = m_watcher->addPath(path);
+
+ // Add all subdirectories
+ QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
+ for (const QFileInfo& entry : entries) {
+ if (addPathRecursively(entry.absoluteFilePath())) {
+ watching = true;
+ }
+ }
+ return watching;
+}
+
+QStringList IconList::getIconFilePaths() const
+{
+ QStringList iconFiles{};
+ QStringList directories{ m_dir.absolutePath() };
+ while (!directories.isEmpty()) {
+ QString first = directories.takeFirst();
+ QDir dir(first);
+ for (QFileInfo& fileInfo : dir.entryInfoList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) {
+ if (fileInfo.isDir())
+ directories.push_back(fileInfo.absoluteFilePath());
+ else
+ iconFiles.push_back(fileInfo.absoluteFilePath());
+ }
+ }
+ return iconFiles;
+}
+
+QString formatName(const QDir& iconsDir, const QFileInfo& iconFile)
+{
+ if (iconFile.dir() == iconsDir)
+ return iconFile.baseName();
+
+ constexpr auto delimiter = " » ";
+ QString relativePathWithoutExtension = iconsDir.relativeFilePath(iconFile.dir().path()) + QDir::separator() + iconFile.baseName();
+ return relativePathWithoutExtension.replace(QDir::separator(), delimiter);
+}
+
+/// Split into a separate function because the preprocessing impedes readability
+QSet toStringSet(const QList& list)
+{
+ QSet set(list.begin(), list.end());
+ return set;
+}
+
void IconList::directoryChanged(const QString& path)
{
- QDir new_dir(path);
- if (m_dir.absolutePath() != new_dir.absolutePath()) {
- m_dir.setPath(path);
+ QDir newDir(path);
+ if (m_dir.absolutePath() != newDir.absolutePath()) {
+ if (!path.startsWith(m_dir.absolutePath()))
+ m_dir.setPath(path);
m_dir.refresh();
- if (is_watching)
+ if (m_isWatching)
stopWatching();
startWatching();
}
- if (!m_dir.exists())
- if (!FS::ensureFolderPathExists(m_dir.absolutePath()))
- return;
+ if (!m_dir.exists() && !FS::ensureFolderPathExists(m_dir.absolutePath()))
+ return;
m_dir.refresh();
- auto new_list = m_dir.entryList(QDir::Files, QDir::Name);
- for (auto it = new_list.begin(); it != new_list.end(); it++) {
- QString& foo = (*it);
- foo = m_dir.filePath(foo);
- }
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- QSet new_set(new_list.begin(), new_list.end());
-#else
- auto new_set = new_list.toSet();
-#endif
- QList current_list;
- for (auto& it : icons) {
+ const QStringList newFileNamesList = getIconFilePaths();
+ const QSet newSet = toStringSet(newFileNamesList);
+ QSet currentSet;
+ for (const MMCIcon& it : m_icons) {
if (!it.has(IconType::FileBased))
continue;
- current_list.push_back(it.m_images[IconType::FileBased].filename);
+ QFileInfo icon(it.getFilePath());
+ currentSet.insert(icon.absoluteFilePath());
}
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- QSet current_set(current_list.begin(), current_list.end());
-#else
- QSet current_set = current_list.toSet();
-#endif
+ QSet toRemove = currentSet - newSet;
+ QSet toAdd = newSet - currentSet;
- QSet to_remove = current_set;
- to_remove -= new_set;
-
- QSet to_add = new_set;
- to_add -= current_set;
-
- for (auto remove : to_remove) {
- qDebug() << "Removing " << remove;
- QFileInfo rmfile(remove);
- QString key = rmfile.completeBaseName();
-
- QString suffix = rmfile.suffix();
- // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well
- if (!IconUtils::isIconSuffix(suffix))
- key = rmfile.fileName();
+ for (const QString& removedPath : toRemove) {
+ qDebug() << "Removing icon " << removedPath;
+ QFileInfo removedFile(removedPath);
+ QString relativePath = m_dir.relativeFilePath(removedFile.absoluteFilePath());
+ QString key = QFileInfo(relativePath).completeBaseName();
int idx = getIconIndex(key);
if (idx == -1)
continue;
- icons[idx].remove(IconType::FileBased);
- if (icons[idx].type() == IconType::ToBeDeleted) {
+ m_icons[idx].remove(FileBased);
+ if (m_icons[idx].type() == ToBeDeleted) {
beginRemoveRows(QModelIndex(), idx, idx);
- icons.remove(idx);
+ m_icons.remove(idx);
reindex();
endRemoveRows();
} else {
dataChanged(index(idx), index(idx));
}
- m_watcher->removePath(remove);
+ m_watcher->removePath(removedPath);
emit iconUpdated(key);
}
- for (auto add : to_add) {
- qDebug() << "Adding " << add;
+ for (const QString& addedPath : toAdd) {
+ qDebug() << "Adding icon " << addedPath;
- QFileInfo addfile(add);
- QString key = addfile.completeBaseName();
+ QFileInfo addfile(addedPath);
+ QString relativePath = m_dir.relativeFilePath(addfile.absoluteFilePath());
+ QString key = QFileInfo(relativePath).completeBaseName();
+ QString name = formatName(m_dir, addfile);
- QString suffix = addfile.suffix();
- // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well
- if (!IconUtils::isIconSuffix(suffix))
- key = addfile.fileName();
-
- if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased)) {
- m_watcher->addPath(add);
+ if (addIcon(key, name, addfile.filePath(), IconType::FileBased)) {
+ m_watcher->addPath(addedPath);
emit iconUpdated(key);
}
}
@@ -171,24 +208,24 @@ void IconList::directoryChanged(const QString& path)
void IconList::fileChanged(const QString& path)
{
- qDebug() << "Checking " << path;
+ qDebug() << "Checking icon " << path;
QFileInfo checkfile(path);
if (!checkfile.exists())
return;
- QString key = checkfile.completeBaseName();
+ QString key = m_dir.relativeFilePath(checkfile.absoluteFilePath());
int idx = getIconIndex(key);
if (idx == -1)
return;
QIcon icon(path);
- if (!icon.availableSizes().size())
+ if (icon.availableSizes().empty())
return;
- icons[idx].m_images[IconType::FileBased].icon = icon;
+ m_icons[idx].m_images[IconType::FileBased].icon = icon;
dataChanged(index(idx), index(idx));
emit iconUpdated(key);
}
-void IconList::SettingChanged(const Setting& setting, QVariant value)
+void IconList::SettingChanged(const Setting& setting, const QVariant& value)
{
if (setting.id() != "IconsDir")
return;
@@ -200,8 +237,8 @@ void IconList::startWatching()
{
auto abs_path = m_dir.absolutePath();
FS::ensureFolderPathExists(abs_path);
- is_watching = m_watcher->addPath(abs_path);
- if (is_watching) {
+ m_isWatching = addPathRecursively(abs_path);
+ if (m_isWatching) {
qDebug() << "Started watching " << abs_path;
} else {
qDebug() << "Failed to start watching " << abs_path;
@@ -212,7 +249,7 @@ void IconList::stopWatching()
{
m_watcher->removePaths(m_watcher->files());
m_watcher->removePaths(m_watcher->directories());
- is_watching = false;
+ m_isWatching = false;
}
QStringList IconList::mimeTypes() const
@@ -242,7 +279,7 @@ bool IconList::dropMimeData(const QMimeData* data,
if (data->hasUrls()) {
auto urls = data->urls();
QStringList iconFiles;
- for (auto url : urls) {
+ for (const auto& url : urls) {
// only local files may be dropped...
if (!url.isLocalFile())
continue;
@@ -263,33 +300,33 @@ Qt::ItemFlags IconList::flags(const QModelIndex& index) const
QVariant IconList::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
- return QVariant();
+ return {};
int row = index.row();
- if (row < 0 || row >= icons.size())
- return QVariant();
+ if (row < 0 || row >= m_icons.size())
+ return {};
switch (role) {
case Qt::DecorationRole:
- return icons[row].icon();
+ return m_icons[row].icon();
case Qt::DisplayRole:
- return icons[row].name();
+ return m_icons[row].name();
case Qt::UserRole:
- return icons[row].m_key;
+ return m_icons[row].m_key;
default:
- return QVariant();
+ return {};
}
}
int IconList::rowCount(const QModelIndex& parent) const
{
- return parent.isValid() ? 0 : icons.size();
+ return parent.isValid() ? 0 : m_icons.size();
}
void IconList::installIcons(const QStringList& iconFiles)
{
- for (QString file : iconFiles)
+ for (const QString& file : iconFiles)
installIcon(file, {});
}
@@ -312,12 +349,13 @@ bool IconList::iconFileExists(const QString& key) const
return iconEntry && iconEntry->has(IconType::FileBased);
}
+/// Returns the icon with the given key or nullptr if it doesn't exist.
const MMCIcon* IconList::icon(const QString& key) const
{
int iconIdx = getIconIndex(key);
if (iconIdx == -1)
return nullptr;
- return &icons[iconIdx];
+ return &m_icons[iconIdx];
}
bool IconList::deleteIcon(const QString& key)
@@ -332,22 +370,22 @@ bool IconList::trashIcon(const QString& key)
bool IconList::addThemeIcon(const QString& key)
{
- auto iter = name_index.find(key);
- if (iter != name_index.end()) {
- auto& oldOne = icons[*iter];
+ auto iter = m_nameIndex.find(key);
+ if (iter != m_nameIndex.end()) {
+ auto& oldOne = m_icons[*iter];
oldOne.replace(Builtin, key);
dataChanged(index(*iter), index(*iter));
return true;
}
// add a new icon
- beginInsertRows(QModelIndex(), icons.size(), icons.size());
+ beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size());
{
MMCIcon mmc_icon;
mmc_icon.m_name = key;
mmc_icon.m_key = key;
mmc_icon.replace(Builtin, key);
- icons.push_back(mmc_icon);
- name_index[key] = icons.size() - 1;
+ m_icons.push_back(mmc_icon);
+ m_nameIndex[key] = m_icons.size() - 1;
}
endInsertRows();
return true;
@@ -359,22 +397,22 @@ bool IconList::addIcon(const QString& key, const QString& name, const QString& p
QIcon icon(path);
if (icon.isNull())
return false;
- auto iter = name_index.find(key);
- if (iter != name_index.end()) {
- auto& oldOne = icons[*iter];
+ auto iter = m_nameIndex.find(key);
+ if (iter != m_nameIndex.end()) {
+ auto& oldOne = m_icons[*iter];
oldOne.replace(type, icon, path);
dataChanged(index(*iter), index(*iter));
return true;
}
// add a new icon
- beginInsertRows(QModelIndex(), icons.size(), icons.size());
+ beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size());
{
MMCIcon mmc_icon;
mmc_icon.m_name = name;
mmc_icon.m_key = key;
mmc_icon.replace(type, icon, path);
- icons.push_back(mmc_icon);
- name_index[key] = icons.size() - 1;
+ m_icons.push_back(mmc_icon);
+ m_nameIndex[key] = m_icons.size() - 1;
}
endInsertRows();
return true;
@@ -389,33 +427,32 @@ void IconList::saveIcon(const QString& key, const QString& path, const char* for
void IconList::reindex()
{
- name_index.clear();
- int i = 0;
- for (auto& iter : icons) {
- name_index[iter.m_key] = i;
- i++;
+ m_nameIndex.clear();
+ for (int i = 0; i < m_icons.size(); i++) {
+ m_nameIndex[m_icons[i].m_key] = i;
+ emit iconUpdated(m_icons[i].m_key); // prevents incorrect indices with proxy model
}
}
QIcon IconList::getIcon(const QString& key) const
{
- int icon_index = getIconIndex(key);
+ int iconIndex = getIconIndex(key);
- if (icon_index != -1)
- return icons[icon_index].icon();
+ if (iconIndex != -1)
+ return m_icons[iconIndex].icon();
- // Fallback for icons that don't exist.
- icon_index = getIconIndex("grass");
+ // Fallback for icons that don't exist.b
+ iconIndex = getIconIndex("grass");
- if (icon_index != -1)
- return icons[icon_index].icon();
- return QIcon();
+ if (iconIndex != -1)
+ return m_icons[iconIndex].icon();
+ return {};
}
int IconList::getIconIndex(const QString& key) const
{
- auto iter = name_index.find(key == "default" ? "grass" : key);
- if (iter != name_index.end())
+ auto iter = m_nameIndex.find(key == "default" ? "grass" : key);
+ if (iter != m_nameIndex.end())
return *iter;
return -1;
@@ -425,3 +462,15 @@ QString IconList::getDirectory() const
{
return m_dir.absolutePath();
}
+
+/// Returns the directory of the icon with the given key or the default directory if it's a builtin icon.
+QString IconList::iconDirectory(const QString& key) const
+{
+ for (const auto& mmcIcon : m_icons) {
+ if (mmcIcon.m_key == key && mmcIcon.has(IconType::FileBased)) {
+ QFileInfo iconFile(mmcIcon.getFilePath());
+ return iconFile.dir().path();
+ }
+ }
+ return getDirectory();
+}
\ No newline at end of file
diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h
index 553946c42..d2f904448 100644
--- a/launcher/icons/IconList.h
+++ b/launcher/icons/IconList.h
@@ -51,7 +51,7 @@ class QFileSystemWatcher;
class IconList : public QAbstractListModel {
Q_OBJECT
public:
- explicit IconList(const QStringList& builtinPaths, QString path, QObject* parent = 0);
+ explicit IconList(const QStringList& builtinPaths, const QString& path, QObject* parent = 0);
virtual ~IconList() {};
QIcon getIcon(const QString& key) const;
@@ -72,6 +72,7 @@ class IconList : public QAbstractListModel {
bool deleteIcon(const QString& key);
bool trashIcon(const QString& key);
bool iconFileExists(const QString& key) const;
+ QString iconDirectory(const QString& key) const;
void installIcons(const QStringList& iconFiles);
void installIcon(const QString& file, const QString& name);
@@ -91,18 +92,20 @@ class IconList : public QAbstractListModel {
IconList& operator=(const IconList&) = delete;
void reindex();
void sortIconList();
+ bool addPathRecursively(const QString& path);
+ QStringList getIconFilePaths() const;
public slots:
void directoryChanged(const QString& path);
protected slots:
void fileChanged(const QString& path);
- void SettingChanged(const Setting& setting, QVariant value);
+ void SettingChanged(const Setting& setting, const QVariant& value);
private:
shared_qobject_ptr m_watcher;
- bool is_watching;
- QMap name_index;
- QVector icons;
+ bool m_isWatching;
+ QMap m_nameIndex;
+ QList m_icons;
QDir m_dir;
};
diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp
index c54a5b04b..0aa725705 100644
--- a/launcher/java/JavaChecker.cpp
+++ b/launcher/java/JavaChecker.cpp
@@ -44,8 +44,8 @@
#include "FileSystem.h"
#include "java/JavaUtils.h"
-JavaChecker::JavaChecker(QString path, QString args, int minMem, int maxMem, int permGen, int id, QObject* parent)
- : Task(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen), m_id(id)
+JavaChecker::JavaChecker(QString path, QString args, int minMem, int maxMem, int permGen, int id)
+ : Task(), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen), m_id(id)
{}
void JavaChecker::executeTask()
@@ -137,11 +137,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status)
QMap results;
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QStringList lines = m_stdout.split("\n", Qt::SkipEmptyParts);
-#else
- QStringList lines = m_stdout.split("\n", QString::SkipEmptyParts);
-#endif
for (QString line : lines) {
line = line.trimmed();
// NOTE: workaround for GH-4125, where garbage is getting printed into stdout on bedrock linux
@@ -149,11 +145,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status)
continue;
}
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
auto parts = line.split('=', Qt::SkipEmptyParts);
-#else
- auto parts = line.split('=', QString::SkipEmptyParts);
-#endif
if (parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) {
continue;
} else {
@@ -171,7 +163,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status)
auto os_arch = results["os.arch"];
auto java_version = results["java.version"];
auto java_vendor = results["java.vendor"];
- bool is_64 = os_arch == "x86_64" || os_arch == "amd64" || os_arch == "aarch64" || os_arch == "arm64";
+ bool is_64 = os_arch == "x86_64" || os_arch == "amd64" || os_arch == "aarch64" || os_arch == "arm64" || os_arch == "riscv64";
result.validity = Result::Validity::Valid;
result.is_64bit = is_64;
diff --git a/launcher/java/JavaChecker.h b/launcher/java/JavaChecker.h
index 171a18b76..a04b68170 100644
--- a/launcher/java/JavaChecker.h
+++ b/launcher/java/JavaChecker.h
@@ -1,7 +1,6 @@
#pragma once
#include
#include
-#include
#include "JavaVersion.h"
#include "QObjectPtr.h"
@@ -26,7 +25,7 @@ class JavaChecker : public Task {
enum class Validity { Errored, ReturnedInvalidData, Valid } validity = Validity::Errored;
};
- explicit JavaChecker(QString path, QString args, int minMem = 0, int maxMem = 0, int permGen = 0, int id = 0, QObject* parent = 0);
+ explicit JavaChecker(QString path, QString args, int minMem = 0, int maxMem = 0, int permGen = 0, int id = 0);
signals:
void checkFinished(const Result& result);
diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp
index 569fda306..aa7fab8a0 100644
--- a/launcher/java/JavaInstallList.cpp
+++ b/launcher/java/JavaInstallList.cpp
@@ -163,7 +163,7 @@ void JavaListLoadTask::executeTask()
JavaUtils ju;
QList candidate_paths = m_only_managed_versions ? getPrismJavaBundle() : ju.FindJavaPaths();
- ConcurrentTask::Ptr job(new ConcurrentTask(this, "Java detection", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()));
+ ConcurrentTask::Ptr job(new ConcurrentTask("Java detection", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()));
m_job.reset(job);
connect(m_job.get(), &Task::finished, this, &JavaListLoadTask::javaCheckerFinished);
connect(m_job.get(), &Task::progress, this, &Task::setProgress);
@@ -171,7 +171,7 @@ void JavaListLoadTask::executeTask()
qDebug() << "Probing the following Java paths: ";
int id = 0;
for (QString candidate : candidate_paths) {
- auto checker = new JavaChecker(candidate, "", 0, 0, 0, id, this);
+ auto checker = new JavaChecker(candidate, "", 0, 0, 0, id);
connect(checker, &JavaChecker::checkFinished, [this](const JavaChecker::Result& result) { m_results << result; });
job->addTask(Task::Ptr(checker));
id++;
diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp
index bc8026348..072cb1d16 100644
--- a/launcher/java/JavaUtils.cpp
+++ b/launcher/java/JavaUtils.cpp
@@ -102,6 +102,8 @@ QProcessEnvironment CleanEnviroment()
QString newValue = stripVariableEntries(key, value, rawenv.value("LAUNCHER_" + key));
qDebug() << "Env: stripped" << key << value << "to" << newValue;
+
+ value = newValue;
}
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
// Strip IBus
@@ -403,7 +405,7 @@ QList JavaUtils::FindJavaPaths()
{
QList javas;
javas.append(this->GetDefaultJava()->path);
- auto scanJavaDir = [&](
+ auto scanJavaDir = [&javas](
const QString& dirPath,
const std::function& filter = [](const QFileInfo&) { return true; }) {
QDir dir(dirPath);
@@ -422,7 +424,7 @@ QList JavaUtils::FindJavaPaths()
};
// java installed in a snap is installed in the standard directory, but underneath $SNAP
auto snap = qEnvironmentVariable("SNAP");
- auto scanJavaDirs = [&](const QString& dirPath) {
+ auto scanJavaDirs = [scanJavaDir, snap](const QString& dirPath) {
scanJavaDir(dirPath);
if (!snap.isNull()) {
scanJavaDir(snap + dirPath);
@@ -440,9 +442,15 @@ QList JavaUtils::FindJavaPaths()
QString fileName = info.fileName();
return fileName.startsWith("openjdk-") || fileName.startsWith("openj9-");
};
+ // AOSC OS's locations for openjdk
+ auto aoscFilter = [](const QFileInfo& info) {
+ QString fileName = info.fileName();
+ return fileName == "java" || fileName.startsWith("java-");
+ };
scanJavaDir("/usr/lib64", gentooFilter);
scanJavaDir("/usr/lib", gentooFilter);
scanJavaDir("/opt", gentooFilter);
+ scanJavaDir("/usr/lib", aoscFilter);
// javas stored in Prism Launcher's folder
scanJavaDirs("java");
// manually installed JDKs in /opt
@@ -544,12 +552,12 @@ QStringList getPrismJavaBundle()
{
QList javas;
- auto scanDir = [&](QString prefix) {
+ auto scanDir = [&javas](QString prefix) {
javas.append(FS::PathCombine(prefix, "jre", "bin", JavaUtils::javaExecutable));
javas.append(FS::PathCombine(prefix, "bin", JavaUtils::javaExecutable));
javas.append(FS::PathCombine(prefix, JavaUtils::javaExecutable));
};
- auto scanJavaDir = [&](const QString& dirPath) {
+ auto scanJavaDir = [scanDir](const QString& dirPath) {
QDir dir(dirPath);
if (!dir.exists())
return;
diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp
index bca50f2c9..e9a160ea7 100644
--- a/launcher/java/JavaVersion.cpp
+++ b/launcher/java/JavaVersion.cpp
@@ -19,9 +19,13 @@ JavaVersion& JavaVersion::operator=(const QString& javaVersionString)
QRegularExpression pattern;
if (javaVersionString.startsWith("1.")) {
- pattern = QRegularExpression("1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?");
+ static const QRegularExpression s_withOne(
+ "1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?");
+ pattern = s_withOne;
} else {
- pattern = QRegularExpression("(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?");
+ static const QRegularExpression s_withoutOne(
+ "(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?");
+ pattern = s_withoutOne;
}
auto match = pattern.match(m_string);
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/java/download/ManifestDownloadTask.cpp b/launcher/java/download/ManifestDownloadTask.cpp
index 836afeaac..20b39e751 100644
--- a/launcher/java/download/ManifestDownloadTask.cpp
+++ b/launcher/java/download/ManifestDownloadTask.cpp
@@ -86,11 +86,10 @@ void ManifestDownloadTask::downloadJava(const QJsonDocument& doc)
if (type == "directory") {
FS::ensureFolderPathExists(file);
} else if (type == "link") {
- // this is linux only !
+ // this is *nix only !
auto path = Json::ensureString(meta, "target");
if (!path.isEmpty()) {
- auto target = FS::PathCombine(file, "../" + path);
- QFile(target).link(file);
+ QFile::link(path, file);
}
} else if (type == "file") {
// TODO download compressed version if it exists ?
diff --git a/launcher/launch/LaunchStep.cpp b/launcher/launch/LaunchStep.cpp
index f3e9dfce0..0b352ea9f 100644
--- a/launcher/launch/LaunchStep.cpp
+++ b/launcher/launch/LaunchStep.cpp
@@ -16,7 +16,7 @@
#include "LaunchStep.h"
#include "LaunchTask.h"
-LaunchStep::LaunchStep(LaunchTask* parent) : Task(parent), m_parent(parent)
+LaunchStep::LaunchStep(LaunchTask* parent) : Task(), m_parent(parent)
{
connect(this, &LaunchStep::readyForLaunch, parent, &LaunchTask::onReadyForLaunch);
connect(this, &LaunchStep::logLine, parent, &LaunchTask::onLogLine);
diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp
index 0251b302d..b67df7631 100644
--- a/launcher/launch/LaunchTask.cpp
+++ b/launcher/launch/LaunchTask.cpp
@@ -37,12 +37,12 @@
#include "launch/LaunchTask.h"
#include
+#include
#include
#include
#include
-#include
-#include
#include
+#include
#include "MessageLevel.h"
#include "tasks/Task.h"
@@ -214,6 +214,52 @@ shared_qobject_ptr LaunchTask::getLogModel()
return m_logModel;
}
+bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel::Enum level)
+{
+ LogParser* parser;
+ switch (level) {
+ case MessageLevel::StdErr:
+ parser = &m_stderrParser;
+ break;
+ case MessageLevel::StdOut:
+ parser = &m_stdoutParser;
+ break;
+ default:
+ return false;
+ }
+
+ parser->appendLine(line);
+ auto items = parser->parseAvailable();
+ if (auto err = parser->getError(); err.has_value()) {
+ auto& model = *getLogModel();
+ model.append(MessageLevel::Error, tr("[Log4j Parse Error] Failed to parse log4j log event: %1").arg(err.value().errMessage));
+ return false;
+ } else {
+ if (!items.isEmpty()) {
+ auto& model = *getLogModel();
+ for (auto const& item : items) {
+ if (std::holds_alternative(item)) {
+ auto entry = std::get(item);
+ auto msg = QString("[%1] [%2/%3] [%4]: %5")
+ .arg(entry.timestamp.toString("HH:mm:ss"))
+ .arg(entry.thread)
+ .arg(entry.levelText)
+ .arg(entry.logger)
+ .arg(entry.message);
+ msg = censorPrivateInfo(msg);
+ model.append(entry.level, msg);
+ } else if (std::holds_alternative(item)) {
+ auto msg = std::get(item).message;
+ level = LogParser::guessLevel(msg, model.previousLevel());
+ msg = censorPrivateInfo(msg);
+ model.append(level, msg);
+ }
+ }
+ }
+ }
+ return true;
+}
+
void LaunchTask::onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel)
{
for (auto& line : lines) {
@@ -223,21 +269,26 @@ void LaunchTask::onLogLines(const QStringList& lines, MessageLevel::Enum default
void LaunchTask::onLogLine(QString line, MessageLevel::Enum level)
{
+ if (parseXmlLogs(line, level)) {
+ return;
+ }
+
// if the launcher part set a log level, use it
auto innerLevel = MessageLevel::fromLine(line);
if (innerLevel != MessageLevel::Unknown) {
level = innerLevel;
}
+ auto& model = *getLogModel();
+
// If the level is still undetermined, guess level
- if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) {
- level = m_instance->guessLevel(line, level);
+ if (level == MessageLevel::Unknown) {
+ level = LogParser::guessLevel(line, model.previousLevel());
}
// censor private user info
line = censorPrivateInfo(line);
- auto& model = *getLogModel();
model.append(level, line);
}
@@ -254,20 +305,60 @@ void LaunchTask::emitFailed(QString reason)
Task::emitFailed(reason);
}
-void LaunchTask::substituteVariables(QStringList& args) const
+QString expandVariables(const QString& input, QProcessEnvironment dict)
{
- auto env = m_instance->createEnvironment();
+ QString result = input;
- for (auto key : env.keys()) {
- args.replaceInStrings("$" + key, env.value(key));
+ enum { base, maybeBrace, variable, brace } state = base;
+ int startIdx = -1;
+ for (int i = 0; i < result.length();) {
+ QChar c = result.at(i++);
+ switch (state) {
+ case base:
+ if (c == '$')
+ state = maybeBrace;
+ break;
+ case maybeBrace:
+ if (c == '{') {
+ state = brace;
+ startIdx = i;
+ } else if (c.isLetterOrNumber() || c == '_') {
+ state = variable;
+ startIdx = i - 1;
+ } else {
+ state = base;
+ }
+ break;
+ case brace:
+ if (c == '}') {
+ const auto res = dict.value(result.mid(startIdx, i - 1 - startIdx), "");
+ if (!res.isEmpty()) {
+ result.replace(startIdx - 2, i - startIdx + 2, res);
+ i = startIdx - 2 + res.length();
+ }
+ state = base;
+ }
+ break;
+ case variable:
+ if (!c.isLetterOrNumber() && c != '_') {
+ const auto res = dict.value(result.mid(startIdx, i - startIdx - 1), "");
+ if (!res.isEmpty()) {
+ result.replace(startIdx - 1, i - startIdx, res);
+ i = startIdx - 1 + res.length();
+ }
+ state = base;
+ }
+ break;
+ }
}
+ if (state == variable) {
+ if (const auto res = dict.value(result.mid(startIdx), ""); !res.isEmpty())
+ result.replace(startIdx - 1, result.length() - startIdx + 1, res);
+ }
+ return result;
}
-void LaunchTask::substituteVariables(QString& cmd) const
+QString LaunchTask::substituteVariables(QString& cmd, bool isLaunch) const
{
- auto env = m_instance->createEnvironment();
-
- for (auto key : env.keys()) {
- cmd.replace("$" + key, env.value(key));
- }
+ return expandVariables(cmd, isLaunch ? m_instance->createLaunchEnvironment() : m_instance->createEnvironment());
}
diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h
index 56065af5b..5effab980 100644
--- a/launcher/launch/LaunchTask.h
+++ b/launcher/launch/LaunchTask.h
@@ -43,6 +43,7 @@
#include "LaunchStep.h"
#include "LogModel.h"
#include "MessageLevel.h"
+#include "logs/LogParser.h"
class LaunchTask : public Task {
Q_OBJECT
@@ -87,8 +88,7 @@ class LaunchTask : public Task {
shared_qobject_ptr getLogModel();
public:
- void substituteVariables(QStringList& args) const;
- void substituteVariables(QString& cmd) const;
+ QString substituteVariables(QString& cmd, bool isLaunch = false) const;
QString censorPrivateInfo(QString in);
protected: /* methods */
@@ -115,6 +115,9 @@ class LaunchTask : public Task {
private: /*methods */
void finalizeSteps(bool successful, const QString& error);
+ protected:
+ bool parseXmlLogs(QString const& line, MessageLevel::Enum level);
+
protected: /* data */
MinecraftInstancePtr m_instance;
shared_qobject_ptr m_logModel;
@@ -123,4 +126,6 @@ class LaunchTask : public Task {
int currentStep = -1;
State state = NotStarted;
qint64 m_pid = -1;
+ LogParser m_stdoutParser;
+ LogParser m_stderrParser;
};
diff --git a/launcher/launch/LogModel.cpp b/launcher/launch/LogModel.cpp
index 23a33ae18..90af9787d 100644
--- a/launcher/launch/LogModel.cpp
+++ b/launcher/launch/LogModel.cpp
@@ -100,7 +100,7 @@ void LogModel::setMaxLines(int maxLines)
return;
}
// otherwise, we need to reorganize the data because it crosses the wrap boundary
- QVector newContent;
+ QList newContent;
newContent.resize(maxLines);
if (m_numLines <= maxLines) {
// if it all fits in the new buffer, just copy it over
@@ -149,3 +149,28 @@ bool LogModel::wrapLines() const
{
return m_lineWrap;
}
+
+void LogModel::setColorLines(bool state)
+{
+ if (m_colorLines != state) {
+ m_colorLines = state;
+ }
+}
+
+bool LogModel::colorLines() const
+{
+ return m_colorLines;
+}
+
+bool LogModel::isOverFlow()
+{
+ return m_numLines >= m_maxLines && m_stopOnOverflow;
+}
+
+MessageLevel::Enum LogModel::previousLevel()
+{
+ if (!m_content.isEmpty()) {
+ return m_content.last().level;
+ }
+ return MessageLevel::Unknown;
+}
diff --git a/launcher/launch/LogModel.h b/launcher/launch/LogModel.h
index 18e51d7e3..4521bac17 100644
--- a/launcher/launch/LogModel.h
+++ b/launcher/launch/LogModel.h
@@ -24,20 +24,25 @@ class LogModel : public QAbstractListModel {
void setMaxLines(int maxLines);
void setStopOnOverflow(bool stop);
void setOverflowMessage(const QString& overflowMessage);
+ bool isOverFlow();
void setLineWrap(bool state);
bool wrapLines() const;
+ void setColorLines(bool state);
+ bool colorLines() const;
+
+ MessageLevel::Enum previousLevel();
enum Roles { LevelRole = Qt::UserRole };
private /* types */:
struct entry {
- MessageLevel::Enum level;
+ MessageLevel::Enum level = MessageLevel::Enum::Unknown;
QString line;
};
private: /* data */
- QVector m_content;
+ QList m_content;
int m_maxLines = 1000;
// first line in the circular buffer
int m_firstLine = 0;
@@ -47,6 +52,7 @@ class LogModel : public QAbstractListModel {
QString m_overflowMessage = "OVERFLOW";
bool m_suspended = false;
bool m_lineWrap = true;
+ bool m_colorLines = true;
private:
Q_DISABLE_COPY(LogModel)
diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp
index 55d13b58c..0f8d27e94 100644
--- a/launcher/launch/steps/CheckJava.cpp
+++ b/launcher/launch/steps/CheckJava.cpp
@@ -94,7 +94,7 @@ void CheckJava::executeTask()
// if timestamps are not the same, or something is missing, check!
if (m_javaSignature != storedSignature || storedVersion.size() == 0 || storedArchitecture.size() == 0 ||
storedRealArchitecture.size() == 0 || storedVendor.size() == 0) {
- m_JavaChecker.reset(new JavaChecker(realJavaPath, "", 0, 0, 0, 0, this));
+ m_JavaChecker.reset(new JavaChecker(realJavaPath, "", 0, 0, 0, 0));
emit logLine(QString("Checking Java version..."), MessageLevel::Launcher);
connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished);
m_JavaChecker->start();
diff --git a/launcher/launch/steps/PostLaunchCommand.cpp b/launcher/launch/steps/PostLaunchCommand.cpp
index 725101224..6b960974e 100644
--- a/launcher/launch/steps/PostLaunchCommand.cpp
+++ b/launcher/launch/steps/PostLaunchCommand.cpp
@@ -47,25 +47,17 @@ PostLaunchCommand::PostLaunchCommand(LaunchTask* parent) : LaunchStep(parent)
void PostLaunchCommand::executeTask()
{
- // FIXME: where to put this?
-#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
- auto args = QProcess::splitCommand(m_command);
- m_parent->substituteVariables(args);
+ auto cmd = m_parent->substituteVariables(m_command);
+ emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher);
+ auto args = QProcess::splitCommand(cmd);
- emit logLine(tr("Running Post-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher);
const QString program = args.takeFirst();
m_process.start(program, args);
-#else
- m_parent->substituteVariables(m_command);
-
- emit logLine(tr("Running Post-Launch command: %1").arg(m_command), MessageLevel::Launcher);
- m_process.start(m_command);
-#endif
}
void PostLaunchCommand::on_state(LoggedProcess::State state)
{
- auto getError = [&]() { return tr("Post-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); };
+ auto getError = [this]() { return tr("Post-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); };
switch (state) {
case LoggedProcess::Aborted:
case LoggedProcess::Crashed:
diff --git a/launcher/launch/steps/PreLaunchCommand.cpp b/launcher/launch/steps/PreLaunchCommand.cpp
index 6d071a66e..7e843ca3f 100644
--- a/launcher/launch/steps/PreLaunchCommand.cpp
+++ b/launcher/launch/steps/PreLaunchCommand.cpp
@@ -47,25 +47,16 @@ PreLaunchCommand::PreLaunchCommand(LaunchTask* parent) : LaunchStep(parent)
void PreLaunchCommand::executeTask()
{
- // FIXME: where to put this?
-#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
- auto args = QProcess::splitCommand(m_command);
- m_parent->substituteVariables(args);
-
- emit logLine(tr("Running Pre-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher);
+ auto cmd = m_parent->substituteVariables(m_command);
+ emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher);
+ auto args = QProcess::splitCommand(cmd);
const QString program = args.takeFirst();
m_process.start(program, args);
-#else
- m_parent->substituteVariables(m_command);
-
- emit logLine(tr("Running Pre-Launch command: %1").arg(m_command), MessageLevel::Launcher);
- m_process.start(m_command);
-#endif
}
void PreLaunchCommand::on_state(LoggedProcess::State state)
{
- auto getError = [&]() { return tr("Pre-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); };
+ auto getError = [this]() { return tr("Pre-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); };
switch (state) {
case LoggedProcess::Aborted:
case LoggedProcess::Crashed:
diff --git a/launcher/logs/LogParser.cpp b/launcher/logs/LogParser.cpp
new file mode 100644
index 000000000..0790dec4d
--- /dev/null
+++ b/launcher/logs/LogParser.cpp
@@ -0,0 +1,351 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#include "LogParser.h"
+
+#include
+#include "MessageLevel.h"
+
+using namespace Qt::Literals::StringLiterals;
+
+void LogParser::appendLine(QAnyStringView data)
+{
+ if (!m_partialData.isEmpty()) {
+ m_buffer = QString(m_partialData);
+ m_buffer.append("\n");
+ m_partialData.clear();
+ }
+ m_buffer.append(data.toString());
+}
+
+std::optional LogParser::getError()
+{
+ return m_error;
+}
+
+std::optional LogParser::parseAttributes()
+{
+ LogParser::LogEntry entry{
+ "",
+ MessageLevel::Info,
+ };
+ auto attributes = m_parser.attributes();
+
+ for (const auto& attr : attributes) {
+ auto name = attr.name();
+ auto value = attr.value();
+ if (name == "logger"_L1) {
+ entry.logger = value.trimmed().toString();
+ } else if (name == "timestamp"_L1) {
+ if (value.trimmed().isEmpty()) {
+ m_parser.raiseError("log4j:Event Missing required attribute: timestamp");
+ return {};
+ }
+ entry.timestamp = QDateTime::fromSecsSinceEpoch(value.trimmed().toLongLong());
+ } else if (name == "level"_L1) {
+ entry.levelText = value.trimmed().toString();
+ entry.level = MessageLevel::getLevel(entry.levelText);
+ } else if (name == "thread"_L1) {
+ entry.thread = value.trimmed().toString();
+ }
+ }
+ if (entry.logger.isEmpty()) {
+ m_parser.raiseError("log4j:Event Missing required attribute: logger");
+ return {};
+ }
+
+ return entry;
+}
+
+void LogParser::setError()
+{
+ m_error = {
+ m_parser.errorString(),
+ m_parser.error(),
+ };
+}
+
+void LogParser::clearError()
+{
+ m_error = {}; // clear previous error
+}
+
+bool isPotentialLog4JStart(QStringView buffer)
+{
+ static QString target = QStringLiteral(" LogParser::parseNext()
+{
+ clearError();
+
+ if (m_buffer.isEmpty()) {
+ return {};
+ }
+
+ if (m_buffer.trimmed().isEmpty()) {
+ auto text = QString(m_buffer);
+ m_buffer.clear();
+ return LogParser::PlainText{ text };
+ }
+
+ // check if we have a full xml log4j event
+ bool isCompleteLog4j = false;
+ m_parser.clear();
+ m_parser.setNamespaceProcessing(false);
+ m_parser.addData(m_buffer);
+ if (m_parser.readNextStartElement()) {
+ if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) {
+ int depth = 1;
+ bool eod = false;
+ while (depth > 0 && !eod) {
+ auto tok = m_parser.readNext();
+ switch (tok) {
+ case QXmlStreamReader::TokenType::StartElement: {
+ depth += 1;
+ } break;
+ case QXmlStreamReader::TokenType::EndElement: {
+ depth -= 1;
+ } break;
+ case QXmlStreamReader::TokenType::EndDocument: {
+ eod = true; // break outer while loop
+ } break;
+ default: {
+ // no op
+ }
+ }
+ if (m_parser.hasError()) {
+ break;
+ }
+ }
+
+ isCompleteLog4j = depth == 0;
+ }
+ }
+
+ if (isCompleteLog4j) {
+ return parseLog4J();
+ } else {
+ if (isPotentialLog4JStart(m_buffer)) {
+ m_partialData = QString(m_buffer);
+ return LogParser::Partial{ QString(m_buffer) };
+ }
+
+ int start = 0;
+ auto bufView = QStringView(m_buffer);
+ while (start < bufView.length()) {
+ if (qsizetype pos = bufView.right(bufView.length() - start).indexOf('<'); pos != -1) {
+ auto slicestart = start + pos;
+ auto slice = bufView.right(bufView.length() - slicestart);
+ if (isPotentialLog4JStart(slice)) {
+ if (slicestart > 0) {
+ auto text = m_buffer.left(slicestart);
+ m_buffer = m_buffer.right(m_buffer.length() - slicestart);
+ if (!text.trimmed().isEmpty()) {
+ return LogParser::PlainText{ text };
+ }
+ }
+ m_partialData = QString(m_buffer);
+ return LogParser::Partial{ QString(m_buffer) };
+ }
+ start = slicestart + 1;
+ } else {
+ break;
+ }
+ }
+
+ // no log4j found, all plain text
+ auto text = QString(m_buffer);
+ m_buffer.clear();
+ return LogParser::PlainText{ text };
+ }
+}
+
+QList LogParser::parseAvailable()
+{
+ QList items;
+ bool doNext = true;
+ while (doNext) {
+ auto item_ = parseNext();
+ if (m_error.has_value()) {
+ return {};
+ }
+ if (item_.has_value()) {
+ auto item = item_.value();
+ if (std::holds_alternative(item)) {
+ break;
+ } else {
+ items.push_back(item);
+ }
+ } else {
+ doNext = false;
+ }
+ }
+ return items;
+}
+
+std::optional LogParser::parseLog4J()
+{
+ m_parser.clear();
+ m_parser.setNamespaceProcessing(false);
+ m_parser.addData(m_buffer);
+
+ m_parser.readNextStartElement();
+ if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) {
+ auto entry_ = parseAttributes();
+ if (!entry_.has_value()) {
+ setError();
+ return {};
+ }
+ auto entry = entry_.value();
+
+ bool foundMessage = false;
+ int depth = 1;
+
+ enum parseOp { noOp, entryReady, parseError };
+
+ auto foundStart = [&]() -> parseOp {
+ depth += 1;
+ if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) {
+ QString message;
+ bool messageComplete = false;
+
+ while (!messageComplete) {
+ auto tok = m_parser.readNext();
+
+ switch (tok) {
+ case QXmlStreamReader::TokenType::Characters: {
+ message.append(m_parser.text());
+ } break;
+ case QXmlStreamReader::TokenType::EndElement: {
+ if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) {
+ messageComplete = true;
+ }
+ } break;
+ case QXmlStreamReader::TokenType::EndDocument: {
+ return parseError; // parse fail
+ } break;
+ default: {
+ // no op
+ }
+ }
+
+ if (m_parser.hasError()) {
+ return parseError;
+ }
+ }
+
+ entry.message = message;
+ foundMessage = true;
+ depth -= 1;
+ }
+ return noOp;
+ };
+
+ auto foundEnd = [&]() -> parseOp {
+ depth -= 1;
+ if (depth == 0 && m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) {
+ if (foundMessage) {
+ auto consumed = m_parser.characterOffset();
+ if (consumed > 0 && consumed <= m_buffer.length()) {
+ m_buffer = m_buffer.right(m_buffer.length() - consumed);
+ // potential whitespace preserved for next item
+ }
+ clearError();
+ return entryReady;
+ }
+ m_parser.raiseError("log4j:Event Missing required attribute: message");
+ setError();
+ return parseError;
+ }
+ return noOp;
+ };
+
+ while (!m_parser.atEnd()) {
+ auto tok = m_parser.readNext();
+ parseOp op = noOp;
+ switch (tok) {
+ case QXmlStreamReader::TokenType::StartElement: {
+ op = foundStart();
+ } break;
+ case QXmlStreamReader::TokenType::EndElement: {
+ op = foundEnd();
+ } break;
+ case QXmlStreamReader::TokenType::EndDocument: {
+ return {};
+ } break;
+ default: {
+ // no op
+ }
+ }
+
+ switch (op) {
+ case parseError:
+ return {}; // parse fail or error
+ case entryReady:
+ return entry;
+ case noOp:
+ default: {
+ // no op
+ }
+ }
+
+ if (m_parser.hasError()) {
+ return {};
+ }
+ }
+ }
+
+ throw std::runtime_error("unreachable: already verified this was a complete log4j:Event");
+}
+
+MessageLevel::Enum LogParser::guessLevel(const QString& line, MessageLevel::Enum level)
+{
+ static const QRegularExpression LINE_WITH_LEVEL("^\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]");
+ auto match = LINE_WITH_LEVEL.match(line);
+ if (match.hasMatch()) {
+ // New style logs from log4j
+ QString timestamp = match.captured("timestamp");
+ QString levelStr = match.captured("level");
+ level = MessageLevel::getLevel(levelStr);
+ } else {
+ // Old style forge logs
+ if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") ||
+ line.contains("[FINEST]"))
+ level = MessageLevel::Info;
+ if (line.contains("[SEVERE]") || line.contains("[STDERR]"))
+ level = MessageLevel::Error;
+ if (line.contains("[WARNING]"))
+ level = MessageLevel::Warning;
+ if (line.contains("[DEBUG]"))
+ level = MessageLevel::Debug;
+ }
+ if (level != MessageLevel::Unknown)
+ return level;
+
+ if (line.contains("overwriting existing"))
+ return MessageLevel::Fatal;
+
+ return MessageLevel::Info;
+}
diff --git a/launcher/logs/LogParser.h b/launcher/logs/LogParser.h
new file mode 100644
index 000000000..1a1d86dd1
--- /dev/null
+++ b/launcher/logs/LogParser.h
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "MessageLevel.h"
+
+class LogParser {
+ public:
+ struct LogEntry {
+ QString logger;
+ MessageLevel::Enum level;
+ QString levelText;
+ QDateTime timestamp;
+ QString thread;
+ QString message;
+ };
+ struct Partial {
+ QString data;
+ };
+ struct PlainText {
+ QString message;
+ };
+ struct Error {
+ QString errMessage;
+ QXmlStreamReader::Error error;
+ };
+
+ using ParsedItem = std::variant;
+
+ public:
+ LogParser() = default;
+
+ void appendLine(QAnyStringView data);
+ std::optional parseNext();
+ QList parseAvailable();
+ std::optional getError();
+
+ /// guess log level from a line of game log
+ static MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level);
+
+ protected:
+ std::optional