Merge remote-tracking branch 'upstream/develop' into rework-settings

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad 2025-05-22 23:04:57 +01:00
commit 618e6bd96b
No known key found for this signature in database
GPG key ID: 5E39D70B4C93C38E
295 changed files with 11681 additions and 2609 deletions

2
.envrc
View file

@ -1,2 +1,2 @@
use flake use nix
watch_file nix/*.nix watch_file nix/*.nix

View file

@ -5,3 +5,9 @@ bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9
# (nix) alejandra -> nixfmt # (nix) alejandra -> nixfmt
4c81d8c53d09196426568c4a31a4e752ed05397a 4c81d8c53d09196426568c4a31a4e752ed05397a
# reformat codebase
1d468ac35ad88d8c77cc83f25e3704d9bd7df01b
# format a part of codebase
5c8481a118c8fefbfe901001d7828eaf6866eac4

View file

@ -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

124
.github/actions/package/linux/action.yml vendored Normal file
View file

@ -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

121
.github/actions/package/macos/action.yml vendored Normal file
View file

@ -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

View file

@ -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

View file

@ -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' }}

View file

@ -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

View file

@ -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"

View file

@ -0,0 +1,73 @@
name: Setup Windows Dependencies
inputs:
build-type:
description: Type for the build
required: true
default: Debug
msystem:
description: MSYS2 subsystem to use
required: false
vcvars-arch:
description: Visual Studio architecture to use
required: true
default: amd64
runs:
using: composite
steps:
# NOTE: Installed on MinGW as well for SignTool
- name: Enter VS Developer shell
if: ${{ runner.os == 'Windows' }}
uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{ inputs.vcvars-arch }}
vsversion: 2022
- name: Setup MSYS2 (MinGW)
if: ${{ inputs.msystem != '' }}
uses: msys2/setup-msys2@v2
with:
msystem: ${{ inputs.msystem }}
update: true
install: >-
git
pacboy: >-
toolchain:p
ccache:p
cmake:p
extra-cmake-modules:p
ninja:p
qt6-base:p
qt6-svg:p
qt6-imageformats:p
qt6-5compat:p
qt6-networkauth:p
cmark:p
tomlplusplus:p
quazip-qt6:p
- name: List pacman packages (MinGW)
if: ${{ inputs.msystem != '' }}
shell: msys2 {0}
run: |
pacman -Qe
- name: Retrieve ccache cache (MinGW)
if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }}
uses: actions/cache@v4.2.3
with:
path: '${{ github.workspace }}\.ccache'
key: ${{ runner.os }}-mingw-w64-ccache-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-mingw-w64-ccache
- name: Setup ccache (MinGW)
if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }}
shell: msys2 {0}
run: |
ccache --set-config=cache_dir='${{ github.workspace }}\.ccache'
ccache --set-config=max_size='500M'
ccache --set-config=compression=true
ccache -p # Show config

View file

@ -64,7 +64,7 @@ jobs:
"prNumber": .number, "prNumber": .number,
"prHeadSha": .head.sha, "prHeadSha": .head.sha,
"prHeadLabel": .head.label, "prHeadLabel": .head.label,
"prBody": .body, "prBody": (.body // ""),
"prLabels": (reduce .labels[].name as $l ([]; . + [$l])) "prLabels": (reduce .labels[].name as $l ([]; . + [$l]))
} }
' <<< "$PR_JSON")" ' <<< "$PR_JSON")"
@ -125,6 +125,7 @@ jobs:
"type": $type, "type": $type,
"number": .number, "number": .number,
"merged": .merged, "merged": .merged,
"state": (if .state == "open" then "Open" elif .merged then "Merged" else "Closed" end),
"labels": (reduce .labels[].name as $l ([]; . + [$l])), "labels": (reduce .labels[].name as $l ([]; . + [$l])),
"basePrUrl": .html_url, "basePrUrl": .html_url,
"baseRepoName": .head.repo.name, "baseRepoName": .head.repo.name,
@ -138,11 +139,16 @@ jobs:
) )
{ {
echo "data=$blocked_pr_data"; echo "data=$blocked_pr_data";
echo "all_merged=$(jq -r 'all(.[].merged; .)' <<< "$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( .merged | not ) | .number )' <<< "$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" } >> "$GITHUB_OUTPUT"
- name: Add 'blocked' Label is Missing - name: Add 'blocked' Label if Missing
id: label_blocked 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) 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 continue-on-error: true
@ -184,14 +190,18 @@ jobs:
# create commit Status, overwrites previous identical context # create commit Status, overwrites previous identical context
while read -r pr_data ; do while read -r pr_data ; do
DESC=$( DESC=$(
jq -r ' "Blocking PR #" + (.number | tostring) + " is " + (if .merged then "" else "not yet " end) + "merged"' <<< "$pr_data" jq -r 'if .type == "Stacked on" then
"Stacked PR #" + (.number | tostring) + " is " + (if .merged then "" else "not yet " end) + "merged"
else
"Blocking PR #" + (.number | tostring) + " is " + (if .state == "Open" then "" else "not yet " end) + "merged or closed"
end ' <<< "$pr_data"
) )
gh api \ gh api \
--method POST \ --method POST \
-H "Accept: application/vnd.github+json" \ -H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \ -H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/${OWNER}/${REPO}/statuses/${pr_head_sha}" \ "/repos/${OWNER}/${REPO}/statuses/${pr_head_sha}" \
-f "state=$(jq -r 'if .merged then "success" else "failure" end' <<< "$pr_data")" \ -f "state=$(jq -r 'if (.type == "Stacked on" and .merged) or (.type == "Blocked on" and (.state != "Open")) then "success" else "failure" end' <<< "$pr_data")" \
-f "target_url=$(jq -r '.basePrUrl' <<< "$pr_data" )" \ -f "target_url=$(jq -r '.basePrUrl' <<< "$pr_data" )" \
-f "description=$DESC" \ -f "description=$DESC" \
-f "context=ci/blocking-pr-check:$(jq '.number' <<< "$pr_data")" -f "context=ci/blocking-pr-check:$(jq '.number' <<< "$pr_data")"
@ -214,7 +224,13 @@ jobs:
base_repo_owner=$(jq -r '.baseRepoOwner' <<< "$pr_data") base_repo_owner=$(jq -r '.baseRepoOwner' <<< "$pr_data")
base_repo_name=$(jq -r '.baseRepoName' <<< "$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" compare_url="https://github.com/$base_repo_owner/$base_repo_name/compare/$base_ref_name...$pr_head_label"
status=$(jq -r 'if .merged then ":heavy_check_mark: Merged" else ":x: Not Merged" end' <<< "$pr_data") status=$(jq -r '
if .type == "Stacked on" then
if .merged then ":heavy_check_mark: Merged" else ":x: Not Merged (" + .state + ")" end
else
if .state != "Open" then ":white_check_mark: " + .state else ":x: Open" end
end
' <<< "$pr_data")
type=$(jq -r '.type' <<< "$pr_data") type=$(jq -r '.type' <<< "$pr_data")
echo " - $type #$base_pr $status [(compare)]($compare_url)" >> "$COMMENT_PATH" echo " - $type #$base_pr $status [(compare)]($compare_url)" >> "$COMMENT_PATH"
done < <(jq -c '.[]' <<< "$BLOCKING_DATA") done < <(jq -c '.[]' <<< "$BLOCKING_DATA")

View file

@ -1,633 +1,199 @@
name: Build name: Build
on: on:
push:
branches-ignore:
- "renovate/**"
paths:
# File types
- "**.cpp"
- "**.h"
- "**.java"
# Directories
- "buildconfig/"
- "cmake/"
- "launcher/"
- "libraries/"
- "program_info/"
- "tests/"
# Files
- "CMakeLists.txt"
- "COPYING.md"
# Workflows
- ".github/workflows/build.yml"
- ".github/actions/package/"
- ".github/actions/setup-dependencies/"
pull_request:
paths:
# File types
- "**.cpp"
- "**.h"
# Directories
- "buildconfig/"
- "cmake/"
- "launcher/"
- "libraries/"
- "program_info/"
- "tests/"
# Files
- "CMakeLists.txt"
- "COPYING.md"
# Workflows
- ".github/workflows/build.yml"
- ".github/actions/package/"
- ".github/actions/setup-dependencies/"
workflow_call: workflow_call:
inputs: inputs:
build_type: build-type:
description: Type of build (Debug, Release, RelWithDebInfo, MinSizeRel) description: Type of build (Debug or Release)
type: string type: string
default: Debug default: Debug
is_qt_cached: workflow_dispatch:
description: Enable Qt caching or not inputs:
build-type:
description: Type of build (Debug or Release)
type: string type: string
default: true default: Debug
secrets:
SPARKLE_ED25519_KEY:
description: Private key for signing Sparkle updates
required: false
WINDOWS_CODESIGN_CERT:
description: Certificate for signing Windows builds
required: false
WINDOWS_CODESIGN_PASSWORD:
description: Password for signing Windows builds
required: false
APPLE_CODESIGN_CERT:
description: Certificate for signing macOS builds
required: false
APPLE_CODESIGN_PASSWORD:
description: Password for signing macOS builds
required: false
APPLE_CODESIGN_ID:
description: Certificate ID for signing macOS builds
required: false
APPLE_NOTARIZE_APPLE_ID:
description: Apple ID used for notarizing macOS builds
required: false
APPLE_NOTARIZE_TEAM_ID:
description: Team ID used for notarizing macOS builds
required: false
APPLE_NOTARIZE_PASSWORD:
description: Password used for notarizing macOS builds
required: false
GPG_PRIVATE_KEY:
description: Private key for AppImage signing
required: false
GPG_PRIVATE_KEY_ID:
description: ID for the GPG_PRIVATE_KEY, to select the signing key
required: false
jobs: jobs:
build: build:
name: Build (${{ matrix.artifact-name }})
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: ubuntu-22.04 - os: ubuntu-22.04
qt_ver: 5 artifact-name: Linux
qt_host: linux base-cmake-preset: linux
qt_arch: ""
qt_version: "5.15.2"
qt_modules: "qtnetworkauth"
- os: ubuntu-22.04
qt_ver: 6
qt_host: linux
qt_arch: ""
qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
linuxdeploy_hash: "4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a linuxdeploy-x86_64.AppImage"
linuxdeploy_qt_hash: "15106be885c1c48a021198e7e1e9a48ce9d02a86dd0a1848f00bdbf3c1c92724 linuxdeploy-plugin-qt-x86_64.AppImage"
appimageupdate_hash: "f1747cf60058e99f1bb9099ee9787d16c10241313b7acec81810ea1b1e568c11 AppImageUpdate-x86_64.AppImage"
- os: windows-2022 - os: windows-2022
name: "Windows-MinGW-w64" artifact-name: Windows-MinGW-w64
msystem: clang64 base-cmake-preset: windows_mingw
vcvars_arch: "amd64_x86" 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 - os: windows-2022
name: "Windows-MSVC" artifact-name: Windows-MSVC
msystem: "" base-cmake-preset: windows_msvc
architecture: "x64" # TODO(@getchoo): This is the default in setup-dependencies/windows. Why isn't it working?!?!
vcvars_arch: "amd64" vcvars-arch: amd64
qt_ver: 6
qt_host: "windows"
qt_arch: "win64_msvc2022_64"
qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
nscurl_tag: "v24.9.26.122"
nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
- os: windows-2022 - os: windows-2022
name: "Windows-MSVC-arm64" artifact-name: Windows-MSVC-arm64
msystem: "" base-cmake-preset: windows_msvc_arm64_cross
architecture: "arm64" vcvars-arch: amd64_arm64
vcvars_arch: "amd64_arm64" qt-architecture: win64_msvc2022_arm64_cross_compiled
qt_ver: 6
qt_host: "windows"
qt_arch: "win64_msvc2022_arm64_cross_compiled"
qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
nscurl_tag: "v24.9.26.122"
nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
- os: macos-14 - os: macos-14
name: macOS artifact-name: macOS
macosx_deployment_target: 11.0 base-cmake-preset: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'macos_universal' || 'macos' }}
qt_ver: 6 macosx-deployment-target: 12.0
qt_host: mac
qt_arch: ""
qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
defaults:
run:
shell: ${{ matrix.msystem != '' && 'msys2 {0}' || 'bash' }}
env: env:
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} 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: steps:
## ##
# PREPARE # SETUP
## ##
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
submodules: "true" submodules: true
- name: "Setup MSYS2" - name: Setup dependencies
if: runner.os == 'Windows' && matrix.msystem != '' id: setup-dependencies
uses: msys2/setup-msys2@v2 uses: ./.github/actions/setup-dependencies
with: with:
build-type: ${{ inputs.build-type || 'Debug' }}
msystem: ${{ matrix.msystem }} msystem: ${{ matrix.msystem }}
update: true vcvars-arch: ${{ matrix.vcvars-arch }}
install: >- qt-architecture: ${{ matrix.qt-architecture }}
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.17
with:
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
- name: Use ccache on Debug builds only
if: inputs.build_type == 'Debug'
shell: bash
run: |
echo "CCACHE_VAR=ccache" >> $GITHUB_ENV
- name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
uses: actions/cache@v4.2.3
with:
path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
restore-keys: |
${{ matrix.os }}-mingw-w64-ccache
- name: Setup ccache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
shell: msys2 {0}
run: |
ccache --set-config=cache_dir='${{ github.workspace }}\.ccache'
ccache --set-config=max_size='500M'
ccache --set-config=compression=true
ccache -p # Show config
ccache -z # Zero stats
- name: Configure ccache (Windows MSVC)
if: ${{ runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug' }}
run: |
# https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix)
Copy-Item C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/ccache.exe -Destination C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/cl.exe
echo "CLToolExe=cl.exe" >> $env:GITHUB_ENV
echo "CLToolPath=C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/" >> $env:GITHUB_ENV
echo "TrackFileAccess=false" >> $env:GITHUB_ENV
# Needed for ccache, but also speeds up compile
echo "UseMultiToolTask=true" >> $env:GITHUB_ENV
- name: Set short version
shell: bash
run: |
ver_short=`git rev-parse --short HEAD`
echo "VERSION=$ver_short" >> $GITHUB_ENV
- name: Install Dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get -y update
sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream libxcb-cursor-dev
- name: Install Dependencies (macOS)
if: runner.os == 'macOS'
run: |
brew update
brew install ninja extra-cmake-modules
- name: Install host Qt (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
uses: jurplel/install-qt-action@v4
with:
aqtversion: "==3.1.*"
py7zrversion: ">=0.20.2"
version: ${{ matrix.qt_version }}
host: "windows"
target: "desktop"
arch: ${{ matrix.qt_arch }}
modules: ${{ matrix.qt_modules }}
cache: ${{ inputs.is_qt_cached }}
cache-key-prefix: host-qt-arm64-windows
dir: ${{ github.workspace }}\HostQt
set-env: false
- name: Install Qt (macOS, Linux & Windows MSVC)
if: matrix.msystem == ''
uses: jurplel/install-qt-action@v4
with:
aqtversion: "==3.1.*"
py7zrversion: ">=0.20.2"
version: ${{ matrix.qt_version }}
target: "desktop"
arch: ${{ matrix.qt_arch }}
modules: ${{ matrix.qt_modules }}
tools: ${{ matrix.qt_tools }}
cache: ${{ inputs.is_qt_cached }}
- name: Install MSVC (Windows MSVC)
if: runner.os == 'Windows' # We want this for MinGW builds as well, as we need SignTool
uses: ilammy/msvc-dev-cmd@v1
with:
vsversion: 2022
arch: ${{ matrix.vcvars_arch }}
- name: Prepare AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5
env:
APPIMAGEUPDATE_HASH: ${{ matrix.appimageupdate_hash }}
LINUXDEPLOY_HASH: ${{ matrix.linuxdeploy_hash }}
LINUXDEPLOY_QT_HASH: ${{ matrix.linuxdeploy_qt_hash }}
run: |
wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage"
wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-x86_64.AppImage"
wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-x86_64.AppImage"
sha256sum -c - <<< "$LINUXDEPLOY_HASH"
sha256sum -c - <<< "$LINUXDEPLOY_QT_HASH"
sha256sum -c - <<< "$APPIMAGEUPDATE_HASH"
sudo apt install libopengl0
- name: Add QT_HOST_PATH var (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
run: |
echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2022_64" >> $env:GITHUB_ENV
- name: Setup java (macOS)
if: runner.os == 'macOS'
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
##
# CONFIGURE
##
- name: Configure CMake (macOS)
if: runner.os == 'macOS' && 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 (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != ''
shell: msys2 {0}
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja
- name: Configure CMake (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture != 'arm64'
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja
- name: Configure CMake (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture == 'arm64'
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }}
- name: Configure CMake (Linux)
if: runner.os == 'Linux'
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -G Ninja
## ##
# BUILD # BUILD
## ##
- name: Build - name: Get CMake preset
if: runner.os != 'Windows' id: cmake-preset
env:
BASE_CMAKE_PRESET: ${{ matrix.base-cmake-preset }}
PRESET_TYPE: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'debug' || 'ci' }}
run: | run: |
cmake --build ${{ env.BUILD_DIR }} echo preset="$BASE_CMAKE_PRESET"_"$PRESET_TYPE" >> "$GITHUB_OUTPUT"
- name: Build (Windows MinGW-w64) - name: Run CMake workflow
if: runner.os == 'Windows' && matrix.msystem != '' env:
shell: msys2 {0} CMAKE_PRESET: ${{ steps.cmake-preset.outputs.preset }}
run: | run: |
cmake --build ${{ env.BUILD_DIR }} cmake --workflow --preset "$CMAKE_PRESET"
- name: Build (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == ''
run: |
cmake --build ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }}
## ##
# TEST # PACKAGE
## ##
- name: Test - name: Get short version
if: runner.os != 'Windows' id: short-version
shell: bash
run: | run: |
ctest -E "^example64|example$" --test-dir build --output-on-failure echo "version=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
- name: Test (Windows MinGW-w64) - name: Package (Linux)
if: runner.os == 'Windows' && matrix.msystem != '' if: ${{ runner.os == 'Linux' }}
shell: msys2 {0} uses: ./.github/actions/package/linux
run: | with:
ctest -E "^example64|example$" --test-dir build --output-on-failure version: ${{ steps.short-version.outputs.version }}
build-type: ${{ steps.setup-dependencies.outputs.build-type }}
cmake-preset: ${{ steps.cmake-preset.outputs.preset }}
qt-version: ${{ steps.setup-dependencies.outputs.qt-version }}
- name: Test (Windows MSVC) gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture != 'arm64' gpg-private-key-id: ${{ secrets.GPG_PRIVATE_KEY_ID }}
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) - name: Package (macOS)
if: runner.os == 'macOS' if: ${{ runner.os == 'macOS' }}
run: | uses: ./.github/actions/package/macos
cmake --install ${{ env.BUILD_DIR }}
cd ${{ env.INSTALL_DIR }}
chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher"
if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then
APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}'
ENTITLEMENTS_FILE='../program_info/App.entitlements'
else
APPLE_CODESIGN_ID='-'
ENTITLEMENTS_FILE='../program_info/AdhocSignedApp.entitlements'
fi
sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "$ENTITLEMENTS_FILE" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
mv "PrismLauncher.app" "Prism Launcher.app"
- name: Notarize (macOS)
if: runner.os == 'macOS'
run: |
cd ${{ env.INSTALL_DIR }}
if [ -n '${{ secrets.APPLE_NOTARIZE_PASSWORD }}' ]; then
ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip
xcrun notarytool submit ../PrismLauncher.zip \
--wait --progress \
--apple-id '${{ secrets.APPLE_NOTARIZE_APPLE_ID }}' \
--team-id '${{ secrets.APPLE_NOTARIZE_TEAM_ID }}' \
--password '${{ secrets.APPLE_NOTARIZE_PASSWORD }}'
xcrun stapler staple "Prism Launcher.app"
else
echo ":warning: Skipping notarization as credentials are not present." >> $GITHUB_STEP_SUMMARY
fi
ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip
- name: Make Sparkle signature (macOS)
if: matrix.name == 'macOS'
run: |
if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then
echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem
signature=$(/opt/homebrew/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n)
rm ed25519-priv.pem
cat >> $GITHUB_STEP_SUMMARY << EOF
### Artifact Information :information_source:
- :memo: Sparkle Signature (ed25519): \`$signature\`
EOF
else
cat >> $GITHUB_STEP_SUMMARY << EOF
### Artifact Information :information_source:
- :warning: Sparkle Signature (ed25519): No private key available (likely a pull request or fork)
EOF
fi
- name: Package (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != ''
shell: msys2 {0}
run: |
cmake --install ${{ env.BUILD_DIR }}
touch ${{ env.INSTALL_DIR }}/manifest.txt
for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt
- name: Package (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == ''
run: |
cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }}
cd ${{ github.workspace }}
Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
- name: Fetch codesign certificate (Windows)
if: runner.os == 'Windows'
shell: bash # yes, we are not using MSYS2 or PowerShell here
run: |
echo '${{ secrets.WINDOWS_CODESIGN_CERT }}' | base64 --decode > codesign.pfx
- name: Sign executable (Windows)
if: runner.os == 'Windows'
run: |
if (Get-Content ./codesign.pfx){
cd ${{ env.INSTALL_DIR }}
# We ship the exact same executable for portable and non-portable editions, so signing just once is fine
SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_updater.exe prismlauncher_filelink.exe
} else {
":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
}
- name: Package (Windows MinGW-w64, portable)
if: runner.os == 'Windows' && matrix.msystem != ''
shell: msys2 {0}
run: |
cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
- name: Package (Windows MSVC, portable)
if: runner.os == 'Windows' && matrix.msystem == ''
run: |
cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
- name: Package (Windows, installer)
if: runner.os == 'Windows'
run: |
if ('${{ matrix.nscurl_tag }}') {
New-Item -Name NSISPlugins -ItemType Directory
Invoke-Webrequest https://github.com/negrutiu/nsis-nscurl/releases/download/${{ matrix.nscurl_tag }}/NScurl.zip -OutFile NSISPlugins\NScurl.zip
$nscurl_hash = Get-FileHash NSISPlugins\NScurl.zip -Algorithm Sha256 | Select-Object -ExpandProperty Hash
if ( $nscurl_hash -ne "${{ matrix.nscurl_sha256 }}") {
echo "::error:: NSCurl.zip sha256 mismatch"
exit 1
}
Expand-Archive -Path NSISPlugins\NScurl.zip -DestinationPath NSISPlugins\NScurl
}
cd ${{ env.INSTALL_DIR }}
makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi"
- name: Sign installer (Windows)
if: runner.os == 'Windows'
run: |
if (Get-Content ./codesign.pfx){
SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe
} else {
":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
}
- name: Package AppImage (Linux)
if: runner.os == 'Linux' && 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.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
export LD_LIBRARY_PATH
chmod +x AppImageUpdate-x86_64.AppImage
cp AppImageUpdate-x86_64.AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin
export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-x86_64.AppImage.zsync"
if [ '${{ secrets.GPG_PRIVATE_KEY_ID }}' != '' ]; then
export SIGN=1
export SIGN_KEY=${{ secrets.GPG_PRIVATE_KEY_ID }}
mkdir -p ~/.gnupg/
echo "$GPG_PRIVATE_KEY" > ~/.gnupg/private.key
gpg --import ~/.gnupg/private.key
else
echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY
fi
./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.prismlauncher.PrismLauncher.svg
mv "PrismLauncher-Linux-x86_64.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage"
- name: Package (Linux, portable)
if: runner.os == 'Linux'
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_PORTABLE_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -DINSTALL_BUNDLE=full -G Ninja
cmake --install ${{ env.BUILD_DIR }}
cmake --install ${{ env.BUILD_DIR }} --component portable
mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libffi.so.*.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
cd ${{ env.INSTALL_PORTABLE_DIR }}
tar -czf ../PrismLauncher-portable.tar.gz *
##
# UPLOAD BUILDS
##
- name: Upload binary tarball (macOS)
if: runner.os == 'macOS'
uses: actions/upload-artifact@v4
with: with:
name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} version: ${{ steps.short-version.outputs.version }}
path: PrismLauncher.zip build-type: ${{ steps.setup-dependencies.outputs.build-type }}
artifact-name: ${{ matrix.artifact-name }}
- name: Upload binary zip (Windows) apple-codesign-cert: ${{ secrets.APPLE-CODESIGN-CERT }}
if: runner.os == 'Windows' apple-codesign-password: ${{ secrets.APPLE-CODESIGN_PASSWORD }}
uses: actions/upload-artifact@v4 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: with:
name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} version: ${{ steps.short-version.outputs.version }}
path: ${{ env.INSTALL_DIR }}/** build-type: ${{ steps.setup-dependencies.outputs.build-type }}
artifact-name: ${{ matrix.artifact-name }}
msystem: ${{ matrix.msystem }}
- name: Upload binary zip (Windows, portable) windows-codesign-cert: ${{ secrets.WINDOWS_CODESIGN_CERT }}
if: runner.os == 'Windows' windows-codesign-password: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
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

View file

@ -1,6 +1,52 @@
name: "CodeQL Code Scanning" 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"
- ".github/actions/setup-dependencies/"
pull_request:
paths:
# File types
- "**.cpp"
- "**.h"
# Directories
- "buildconfig/"
- "cmake/"
- "launcher/"
- "libraries/"
- "program_info/"
- "tests/"
# Files
- "CMakeLists.txt"
- "COPYING.md"
# Workflows
- ".github/codeql"
- ".github/workflows/codeql.yml"
- ".github/actions/setup-dependencies/"
workflow_dispatch:
jobs: jobs:
CodeQL: CodeQL:
@ -10,7 +56,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
submodules: 'true' submodules: "true"
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v3
@ -19,17 +65,15 @@ jobs:
queries: security-and-quality queries: security-and-quality
languages: cpp, java languages: cpp, java
- name: Install Dependencies - name: Setup dependencies
run: uses: ./.github/actions/setup-dependencies
sudo apt-get -y update with:
build-type: Debug
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 libqt5opengl5 libqt5opengl5-dev
- name: Configure and Build - name: Configure and Build
run: | run: |
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr -DLauncher_QT_VERSION_MAJOR=5 -G Ninja cmake --preset linux_debug
cmake --build --preset linux_debug
cmake --build build
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v3

View file

@ -2,22 +2,55 @@ name: Flatpak
on: on:
push: push:
paths-ignore:
- "**.md"
- "**/LICENSE"
- ".github/ISSUE_TEMPLATE/**"
- ".markdownlint**"
- "nix/**"
# We don't do anything with these artifacts on releases. They go to Flathub # We don't do anything with these artifacts on releases. They go to Flathub
tags-ignore: 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: pull_request:
paths-ignore: paths:
- "**.md" # File types
- "**/LICENSE" - "**.cpp"
- ".github/ISSUE_TEMPLATE/**" - "**.h"
- ".markdownlint**"
- "nix/**" # Build files
- "flatpak/"
# Directories
- "buildconfig/"
- "cmake/"
- "launcher/"
- "libraries/"
- "program_info/"
- "tests/"
# Files
- "CMakeLists.txt"
- "COPYING.md"
# Workflows
- ".github/workflows/flatpak.yml"
workflow_dispatch: workflow_dispatch:
permissions: permissions:

View file

@ -1,9 +1,15 @@
name: Merged Blocking Pull Request Automation name: Merged Blocking Pull Request Automation
on: on:
pull_request: pull_request_target:
types: types:
- closed - closed
workflow_dispatch:
inputs:
pr_id:
description: Local Pull Request number to work on
required: true
type: number
jobs: jobs:
update-blocked-status: update-blocked-status:
@ -12,7 +18,7 @@ jobs:
# a pr that was a `blocking:<id>` label was merged. # a pr that was a `blocking:<id>` label was merged.
# find the open pr's it was blocked by and trigger a refresh of their state # find the open pr's it was blocked by and trigger a refresh of their state
if: github.event.pull_request.merged == true && contains( join( github.event.pull_request.labels.*.name, ',' ), 'blocking' ) if: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'blocking') }}
steps: steps:
- name: Generate token - name: Generate token
@ -26,11 +32,11 @@ jobs:
id: gather_deps id: gather_deps
env: env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }} GH_TOKEN: ${{ steps.generate-token.outputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ inputs.pr_id || github.event.pull_request.number }}
run: | run: |
blocked_prs=$( blocked_prs=$(
gh -R ${{ github.repository }} pr list --label 'blocked' --json 'number,body' \ gh -R ${{ github.repository }} pr list --label 'blocked' --json 'number,body' \
| jq -c --argjson pr "${{ github.event.pull_request.number }}" ' | jq -c --argjson pr "$PR_NUMBER" '
reduce ( .[] | select( reduce ( .[] | select(
.body | .body |
scan("(?:blocked (?:by|on)|stacked on):? #([0-9]+)") | scan("(?:blocked (?:by|on)|stacked on):? #([0-9]+)") |

View file

@ -4,28 +4,56 @@ on:
push: push:
tags: tags:
- "*" - "*"
paths-ignore: paths:
- ".github/**" # File types
- "!.github/workflows/nix.yml" - "**.cpp"
- "flatpak/" - "**.h"
- "scripts/" - "**.java"
- ".git*" # Build files
- ".envrc" - "**.nix"
- "**.md" - "nix/"
- "!COPYING.md" - "flake.lock"
- "renovate.json"
# Directories
- "buildconfig/"
- "cmake/"
- "launcher/"
- "libraries/"
- "program_info/"
- "tests/"
# Files
- "CMakeLists.txt"
- "COPYING.md"
# Workflows
- ".github/workflows/nix.yml"
pull_request_target: pull_request_target:
paths-ignore: paths:
- ".github/**" # File types
- "flatpak/" - "**.cpp"
- "scripts/" - "**.h"
- ".git*" # Build files
- ".envrc" - "**.nix"
- "**.md" - "nix/"
- "!COPYING.md" - "flake.lock"
- "renovate.json"
# Directories
- "buildconfig/"
- "cmake/"
- "launcher/"
- "libraries/"
- "program_info/"
- "tests/"
# Files
- "CMakeLists.txt"
- "COPYING.md"
# Workflows
- ".github/workflows/nix.yml"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -61,11 +89,22 @@ jobs:
id-token: write id-token: write
steps: 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 - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: ${{ steps.merge-commit.outputs.merge-commit-sha || github.sha }}
- name: Install Nix - name: Install Nix
uses: DeterminateSystems/nix-installer-action@v16 uses: DeterminateSystems/nix-installer-action@v17
with: with:
determinate: ${{ env.USE_DETERMINATE }} determinate: ${{ env.USE_DETERMINATE }}

View file

@ -10,20 +10,8 @@ jobs:
name: Build Release name: Build Release
uses: ./.github/workflows/build.yml uses: ./.github/workflows/build.yml
with: with:
build_type: Release build-type: Release
is_qt_cached: false secrets: inherit
secrets:
SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }}
APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }}
APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }}
APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}
create_release: create_release:
needs: build_release needs: build_release
@ -46,7 +34,6 @@ jobs:
run: | run: |
mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }} mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }}
mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz
mv PrismLauncher-Linux-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/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage
mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync
mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip
@ -79,6 +66,17 @@ jobs:
cd .. cd ..
done done
for d in PrismLauncher-Windows-MinGW-arm64*; do
cd "${d}" || continue
INST="$(echo -n ${d} | grep -o Setup || true)"
PORT="$(echo -n ${d} | grep -o Portable || true)"
NAME="PrismLauncher-Windows-MinGW-arm64"
test -z "${PORT}" || NAME="${NAME}-Portable"
test -z "${INST}" || mv PrismLauncher-*.exe ../${NAME}-Setup-${{ env.VERSION }}.exe
test -n "${INST}" || zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" *
cd ..
done
- name: Create release - name: Create release
id: create_release id: create_release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
@ -89,13 +87,15 @@ jobs:
draft: true draft: true
prerelease: false prerelease: false
files: | files: |
PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz
PrismLauncher-Linux-x86_64.AppImage PrismLauncher-Linux-x86_64.AppImage
PrismLauncher-Linux-x86_64.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync
PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz
PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip
PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MinGW-w64-Setup-${{ env.VERSION }}.exe 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-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-arm64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-arm64-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-arm64-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MSVC-arm64-Setup-${{ env.VERSION }}.exe

View file

@ -1,42 +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 }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}

View file

@ -17,7 +17,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: cachix/install-nix-action@d1ca217b388ee87b2507a9a93bf01368bde7cec2 # v31 - uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
- uses: DeterminateSystems/update-flake-lock@v24 - uses: DeterminateSystems/update-flake-lock@v24
with: with:

1
.gitignore vendored
View file

@ -14,6 +14,7 @@ CMakeLists.txt.user.*
CMakeSettings.json CMakeSettings.json
/CMakeFiles /CMakeFiles
CMakeCache.txt CMakeCache.txt
CMakeUserPresets.json
/.project /.project
/.settings /.settings
/.idea /.idea

3
.gitmodules vendored
View file

@ -19,3 +19,6 @@
[submodule "flatpak/shared-modules"] [submodule "flatpak/shared-modules"]
path = flatpak/shared-modules path = flatpak/shared-modules
url = https://github.com/flathub/shared-modules.git 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

View file

@ -88,10 +88,8 @@ else()
endif() endif()
endif() endif()
# Fix build with Qt 5.13 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_WARN_DEPRECATED_UP_TO=0x060200")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_UP_TO=0x060000")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00")
# Fix aarch64 build for toml++ # Fix aarch64 build for toml++
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0")
@ -310,23 +308,7 @@ endif()
# Find the required Qt parts # Find the required Qt parts
include(QtVersionlessBackport) include(QtVersionlessBackport)
if(Launcher_QT_VERSION_MAJOR EQUAL 5) if(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_VERSION_MAJOR 5)
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml NetworkAuth OpenGL)
find_package(Qt5 COMPONENTS DBus)
list(APPEND Launcher_QT_DBUS Qt5::DBus)
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)
set(QT_VERSION_MAJOR 6) set(QT_VERSION_MAJOR 6)
find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth OpenGL) find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth OpenGL)
find_package(Qt6 COMPONENTS DBus) find_package(Qt6 COMPONENTS DBus)
@ -344,22 +326,12 @@ else()
message(FATAL_ERROR "Qt version ${Launcher_QT_VERSION_MAJOR} is not supported") message(FATAL_ERROR "Qt version ${Launcher_QT_VERSION_MAJOR} is not supported")
endif() endif()
if(Launcher_QT_VERSION_MAJOR EQUAL 5) if(Launcher_QT_VERSION_MAJOR EQUAL 6)
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()
set(QT_PLUGINS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_PLUGINS}) 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_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}) set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS})
endif() 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) if(NOT Launcher_FORCE_BUNDLED_LIBS)
# Find toml++ # Find toml++
find_package(tomlplusplus 3.2.0 QUIET) find_package(tomlplusplus 3.2.0 QUIET)
@ -503,6 +475,7 @@ add_subdirectory(libraries/libnbtplusplus)
add_subdirectory(libraries/systeminfo) # system information library add_subdirectory(libraries/systeminfo) # system information library
add_subdirectory(libraries/launcher) # java based launcher part for Minecraft add_subdirectory(libraries/launcher) # java based launcher part for Minecraft
add_subdirectory(libraries/javacheck) # java compatibility checker add_subdirectory(libraries/javacheck) # java compatibility checker
add_subdirectory(libraries/qt-qrcodegenerator) # qr code generator
if(FORCE_BUNDLED_ZLIB) if(FORCE_BUNDLED_ZLIB)
message(STATUS "Using bundled zlib") message(STATUS "Using bundled zlib")

14
CMakePresets.json Normal file
View file

@ -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"
]
}

View file

@ -108,7 +108,7 @@
Information on third party licenses used in MinGW-w64 can be found in its COPYING.MinGW-w64-runtime.txt. 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. Copyright (C) 2022 The Qt Company Ltd and other contributors.
Contact: https://www.qt.io/licensing Contact: https://www.qt.io/licensing
@ -403,3 +403,12 @@
You should have received a copy of the GNU Lesser General Public You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>. License along with this library. If not, see <http://www.gnu.org/licenses/>.
## 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.

View file

@ -76,7 +76,13 @@ We thank all the wonderful backers over at Open Collective! Support Prism Launch
Thanks to JetBrains for providing us a few licenses for all their products, as part of their [Open Source program](https://www.jetbrains.com/opensource/). Thanks to JetBrains for providing us a few licenses for all their products, as part of their [Open Source program](https://www.jetbrains.com/opensource/).
[![JetBrains](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/opensource/) <a href="https://jb.gg/OpenSource">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.jetbrains.com/company/brand/img/logo_jb_dos_4.svg">
<source media="(prefers-color-scheme: light)" srcset="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg">
<img alt="JetBrains logo" src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg" width="40%">
</picture>
</a>
Thanks to Weblate for hosting our translation efforts. Thanks to Weblate for hosting our translation efforts.

View file

@ -53,7 +53,6 @@ Config::Config()
LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@"; LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@";
USER_AGENT = "@Launcher_UserAgent@"; USER_AGENT = "@Launcher_UserAgent@";
USER_AGENT_UNCACHED = USER_AGENT + " (Uncached)";
// Version information // Version information
VERSION_MAJOR = @Launcher_VERSION_MAJOR@; VERSION_MAJOR = @Launcher_VERSION_MAJOR@;

View file

@ -107,9 +107,6 @@ class Config {
/// User-Agent to use. /// User-Agent to use.
QString USER_AGENT; QString USER_AGENT;
/// User-Agent to use for uncached requests.
QString USER_AGENT_UNCACHED;
/// The git commit hash of this build /// The git commit hash of this build
QString GIT_COMMIT; QString GIT_COMMIT;

81
cmake/commonPresets.json Normal file
View file

@ -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"
]
}
]
}

180
cmake/linuxPreset.json Normal file
View file

@ -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"
}
]
}
]
}

272
cmake/macosPreset.json Normal file
View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

View file

@ -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"
}
]
}
]
}

31
flake.lock generated
View file

@ -3,11 +3,11 @@
"libnbtplusplus": { "libnbtplusplus": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1699286814, "lastModified": 1744811532,
"narHash": "sha256-yy0q+bky80LtK1GWzz7qpM+aAGrOqLuewbid8WT1ilk=", "narHash": "sha256-qhmjaRkt+O7A+gu6HjUkl7QzOEb4r8y8vWZMG2R/C6o=",
"owner": "PrismLauncher", "owner": "PrismLauncher",
"repo": "libnbtplusplus", "repo": "libnbtplusplus",
"rev": "23b955121b8217c1c348a9ed2483167a6f3ff4ad", "rev": "531449ba1c930c98e0bcf5d332b237a8566f9d78",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -18,11 +18,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1744463964, "lastModified": 1746663147,
"narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=", "narHash": "sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ+TCkTRpRc=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650", "rev": "dda3dcd3fe03e991015e9a74b22d35950f264a54",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -32,10 +32,27 @@
"type": "github" "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": { "root": {
"inputs": { "inputs": {
"libnbtplusplus": "libnbtplusplus", "libnbtplusplus": "libnbtplusplus",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs",
"qt-qrcodegenerator": "qt-qrcodegenerator"
} }
} }
}, },

View file

@ -15,6 +15,11 @@
url = "github:PrismLauncher/libnbtplusplus"; url = "github:PrismLauncher/libnbtplusplus";
flake = false; flake = false;
}; };
qt-qrcodegenerator = {
url = "github:nayuki/QR-Code-generator";
flake = false;
};
}; };
outputs = outputs =
@ -22,6 +27,7 @@
self, self,
nixpkgs, nixpkgs,
libnbtplusplus, libnbtplusplus,
qt-qrcodegenerator,
}: }:
let let
@ -132,6 +138,8 @@
{ {
default = pkgs.mkShell { default = pkgs.mkShell {
name = "prism-launcher";
inputsFrom = [ packages'.prismlauncher-unwrapped ]; inputsFrom = [ packages'.prismlauncher-unwrapped ];
packages = with pkgs; [ packages = with pkgs; [
@ -167,6 +175,7 @@
prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix {
inherit inherit
libnbtplusplus libnbtplusplus
qt-qrcodegenerator
self self
; ;
}; };

@ -1 +1 @@
Subproject commit f5d368a31d6ef046eb2955c74ec6f54f32ed5c4e Subproject commit 73f08ed2c3187f6648ca04ebef030930a6c9f0be

View file

@ -97,6 +97,7 @@
#include <QList> #include <QList>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QStringList> #include <QStringList>
#include <QStringLiteral>
#include <QStyleFactory> #include <QStyleFactory>
#include <QTranslator> #include <QTranslator>
#include <QWindow> #include <QWindow>
@ -128,6 +129,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <sys.h> #include <sys.h>
#include <QStringLiteral>
#include "SysInfo.h" #include "SysInfo.h"
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
@ -154,11 +156,16 @@
#endif #endif
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h> #include <windows.h>
#include <QStyleHints> #include <QStyleHints>
#include "WindowsConsole.h" #include "console/WindowsConsole.h"
#endif #endif
#include "console/Console.h"
#define STRINGIFY(x) #x #define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x) #define TOSTRING(x) STRINGIFY(x)
@ -166,6 +173,63 @@ static const QLatin1String liveCheckFile("live.check");
PixmapCache* PixmapCache::s_instance = nullptr; 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 { namespace {
/** This is used so that we can output to the log file in addition to the CLI. */ /** This is used so that we can output to the log file in addition to the CLI. */
@ -174,11 +238,24 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt
static std::mutex loggerMutex; static std::mutex loggerMutex;
const std::lock_guard<std::mutex> lock(loggerMutex); // synchronized, QFile logFile is not thread-safe const std::lock_guard<std::mutex> 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); QString out = qFormatLogMessage(type, context, msg);
out += QChar::LineFeed; out += QChar::LineFeed;
APPLICATION->logFile->write(out.toUtf8()); APPLICATION->logFile->write(out.toUtf8());
APPLICATION->logFile->flush(); APPLICATION->logFile->flush();
if (isANSIColorConsole) {
// format ansi for console;
qSetMessagePattern(ansiLogFormat);
out = qFormatLogMessage(type, context, msg);
out += QChar::LineFeed;
}
QTextStream(stderr) << out.toLocal8Bit(); QTextStream(stderr) << out.toLocal8Bit();
fflush(stderr); fflush(stderr);
} }
@ -219,8 +296,18 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// attach the parent console if stdout not already captured // attach the parent console if stdout not already captured
if (AttachWindowsConsole()) { if (AttachWindowsConsole()) {
consoleAttached = true; 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 #endif
setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationName(BuildConfig.LAUNCHER_NAME);
setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN);
setApplicationName(BuildConfig.LAUNCHER_NAME); setApplicationName(BuildConfig.LAUNCHER_NAME);
@ -449,27 +536,14 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
return; return;
} }
qInstallMessageHandler(appDebugOutput); qInstallMessageHandler(appDebugOutput);
qSetMessagePattern(defaultLogFormat);
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}");
bool foundLoggingRules = false; bool foundLoggingRules = false;
auto logRulesFile = QStringLiteral("qtlogging.ini"); auto logRulesFile = QStringLiteral("qtlogging.ini");
auto logRulesPath = FS::PathCombine(dataPath, logRulesFile); auto logRulesPath = FS::PathCombine(dataPath, logRulesFile);
qDebug() << "Testing" << logRulesPath << "..."; qInfo() << "Testing" << logRulesPath << "...";
foundLoggingRules = QFile::exists(logRulesPath); foundLoggingRules = QFile::exists(logRulesPath);
// search the dataPath() // search the dataPath()
@ -477,7 +551,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) { if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) {
logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile));
if (!logRulesPath.isEmpty()) { if (!logRulesPath.isEmpty()) {
qDebug() << "Found" << logRulesPath << "..."; qInfo() << "Found" << logRulesPath << "...";
foundLoggingRules = true; foundLoggingRules = true;
} }
} }
@ -488,28 +562,28 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
#else #else
logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); logRulesPath = FS::PathCombine(m_rootPath, logRulesFile);
#endif #endif
qDebug() << "Testing" << logRulesPath << "..."; qInfo() << "Testing" << logRulesPath << "...";
foundLoggingRules = QFile::exists(logRulesPath); foundLoggingRules = QFile::exists(logRulesPath);
} }
if (foundLoggingRules) { if (foundLoggingRules) {
// load and set logging rules // load and set logging rules
qDebug() << "Loading logging rules from:" << logRulesPath; qInfo() << "Loading logging rules from:" << logRulesPath;
QSettings loggingRules(logRulesPath, QSettings::IniFormat); QSettings loggingRules(logRulesPath, QSettings::IniFormat);
loggingRules.beginGroup("Rules"); loggingRules.beginGroup("Rules");
QStringList rule_names = loggingRules.childKeys(); QStringList rule_names = loggingRules.childKeys();
QStringList rules; QStringList rules;
qDebug() << "Setting log rules:"; qInfo() << "Setting log rules:";
for (auto rule_name : rule_names) { for (auto rule_name : rule_names) {
auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString()); auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString());
rules.append(rule); rules.append(rule);
qDebug() << " " << rule; qInfo() << " " << rule;
} }
auto rules_str = rules.join("\n"); auto rules_str = rules.join("\n");
QLoggingCategory::setFilterRules(rules_str); QLoggingCategory::setFilterRules(rules_str);
} }
qDebug() << "<> Log initialized."; qInfo() << "<> Log initialized.";
} }
{ {
@ -526,33 +600,33 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
} }
{ {
qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); qInfo() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
qDebug() << "Version : " << BuildConfig.printableVersionString(); qInfo() << "Version : " << BuildConfig.printableVersionString();
qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM; qInfo() << "Platform : " << BuildConfig.BUILD_PLATFORM;
qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; qInfo() << "Git commit : " << BuildConfig.GIT_COMMIT;
qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC; qInfo() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
qDebug() << "Compiled for : " << BuildConfig.systemID(); qInfo() << "Compiled for : " << BuildConfig.systemID();
qDebug() << "Compiled by : " << BuildConfig.compilerID(); qInfo() << "Compiled by : " << BuildConfig.compilerID();
qDebug() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT; qInfo() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT;
qDebug() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No"); qInfo() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No");
if (adjustedBy.size()) { if (adjustedBy.size()) {
qDebug() << "Work dir before adjustment : " << origcwdPath; qInfo() << "Work dir before adjustment : " << origcwdPath;
qDebug() << "Work dir after adjustment : " << QDir::currentPath(); qInfo() << "Work dir after adjustment : " << QDir::currentPath();
qDebug() << "Adjusted by : " << adjustedBy; qInfo() << "Adjusted by : " << adjustedBy;
} else { } else {
qDebug() << "Work dir : " << QDir::currentPath(); qInfo() << "Work dir : " << QDir::currentPath();
} }
qDebug() << "Binary path : " << binPath; qInfo() << "Binary path : " << binPath;
qDebug() << "Application root path : " << m_rootPath; qInfo() << "Application root path : " << m_rootPath;
if (!m_instanceIdToLaunch.isEmpty()) { if (!m_instanceIdToLaunch.isEmpty()) {
qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch; qInfo() << "ID of instance to launch : " << m_instanceIdToLaunch;
} }
if (!m_serverToJoin.isEmpty()) { 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()) { } 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) { if (m_liveCheck) {
@ -820,7 +894,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
PixmapCache::setInstance(new PixmapCache(this)); PixmapCache::setInstance(new PixmapCache(this));
qDebug() << "<> Settings loaded."; qInfo() << "<> Settings loaded.";
} }
#ifndef QT_NO_ACCESSIBILITY #ifndef QT_NO_ACCESSIBILITY
@ -836,7 +910,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
QString user = settings()->get("ProxyUser").toString(); QString user = settings()->get("ProxyUser").toString();
QString pass = settings()->get("ProxyPass").toString(); QString pass = settings()->get("ProxyPass").toString();
updateProxySettings(proxyTypeStr, addr, port, user, pass); updateProxySettings(proxyTypeStr, addr, port, user, pass);
qDebug() << "<> Network done."; qInfo() << "<> Network done.";
} }
// load translations // load translations
@ -844,8 +918,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_translations.reset(new TranslationsModel("translations")); m_translations.reset(new TranslationsModel("translations"));
auto bcp47Name = m_settings->get("Language").toString(); auto bcp47Name = m_settings->get("Language").toString();
m_translations->selectLanguage(bcp47Name); m_translations->selectLanguage(bcp47Name);
qDebug() << "Your language is" << bcp47Name; qInfo() << "Your language is" << bcp47Name;
qDebug() << "<> Translations loaded."; qInfo() << "<> Translations loaded.";
} }
// Instance icons // Instance icons
@ -856,7 +930,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_icons.reset(new IconList(instFolders, setting->get().toString())); m_icons.reset(new IconList(instFolders, setting->get().toString()));
connect(setting.get(), &Setting::SettingChanged, connect(setting.get(), &Setting::SettingChanged,
[this](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); }); [this](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); });
qDebug() << "<> Instance icons initialized."; qInfo() << "<> Instance icons initialized.";
} }
// Themes // Themes
@ -868,25 +942,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 // 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) // and remember that we have to show him a dialog when the gui starts (if it does so)
QString instDir = InstDirSetting->get().toString(); QString instDir = InstDirSetting->get().toString();
qDebug() << "Instance path : " << instDir; qInfo() << "Instance path : " << instDir;
if (FS::checkProblemticPathJava(QDir(instDir))) { if (FS::checkProblemticPathJava(QDir(instDir))) {
qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!"; qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!";
} }
m_instances.reset(new InstanceList(m_settings, instDir, this)); m_instances.reset(new InstanceList(m_settings, instDir, this));
connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged); connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged);
qDebug() << "Loading Instances..."; qInfo() << "Loading Instances...";
m_instances->loadList(); m_instances->loadList();
qDebug() << "<> Instances loaded."; qInfo() << "<> Instances loaded.";
} }
// and accounts // and accounts
{ {
m_accounts.reset(new AccountList(this)); m_accounts.reset(new AccountList(this));
qDebug() << "Loading accounts..."; qInfo() << "Loading accounts...";
m_accounts->setListFilePath("accounts.json", true); m_accounts->setListFilePath("accounts.json", true);
m_accounts->loadList(); m_accounts->loadList();
m_accounts->fillQueue(); m_accounts->fillQueue();
qDebug() << "<> Accounts loaded."; qInfo() << "<> Accounts loaded.";
} }
// init the http meta cache // init the http meta cache
@ -907,7 +981,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_metacache->addBase("meta", QDir("meta").absolutePath()); m_metacache->addBase("meta", QDir("meta").absolutePath());
m_metacache->addBase("java", QDir("cache/java").absolutePath()); m_metacache->addBase("java", QDir("cache/java").absolutePath());
m_metacache->Load(); m_metacache->Load();
qDebug() << "<> Cache initialized."; qInfo() << "<> Cache initialized.";
} }
// now we have network, download translation updates // now we have network, download translation updates
@ -1811,17 +1885,6 @@ QString Application::getUserAgent()
return BuildConfig.USER_AGENT; return BuildConfig.USER_AGENT;
} }
QString Application::getUserAgentUncached()
{
QString uaOverride = m_settings->get("UserAgentOverride").toString();
if (!uaOverride.isEmpty()) {
uaOverride += " (Uncached)";
return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString());
}
return BuildConfig.USER_AGENT_UNCACHED;
}
bool Application::handleDataMigration(const QString& currentData, bool Application::handleDataMigration(const QString& currentData,
const QString& oldData, const QString& oldData,
const QString& name, const QString& name,

View file

@ -160,7 +160,6 @@ class Application : public QApplication {
QString getFlameAPIKey(); QString getFlameAPIKey();
QString getModrinthAPIToken(); QString getModrinthAPIToken();
QString getUserAgent(); QString getUserAgent();
QString getUserAgentUncached();
/// this is the root of the 'installation'. Used for automatic updates /// this is the root of the 'installation'. Used for automatic updates
const QString& root() { return m_rootPath; } const QString& root() { return m_rootPath; }

View file

@ -42,8 +42,8 @@
#include <QFileInfo> #include <QFileInfo>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QRegularExpression>
#include "Application.h"
#include "settings/INISettingsObject.h" #include "settings/INISettingsObject.h"
#include "settings/OverrideSetting.h" #include "settings/OverrideSetting.h"
#include "settings/Setting.h" #include "settings/Setting.h"
@ -174,6 +174,12 @@ void BaseInstance::copyManagedPack(BaseInstance& other)
m_settings->set("ManagedPackName", other.getManagedPackName()); m_settings->set("ManagedPackName", other.getManagedPackName());
m_settings->set("ManagedPackVersionID", other.getManagedPackVersionID()); m_settings->set("ManagedPackVersionID", other.getManagedPackVersionID());
m_settings->set("ManagedPackVersionName", other.getManagedPackVersionName()); 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 int BaseInstance::getConsoleMaxLines() const

View file

@ -151,9 +151,6 @@ class BaseInstance : public QObject, public std::enable_shared_from_this<BaseIns
void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version); void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version);
void copyManagedPack(BaseInstance& other); void copyManagedPack(BaseInstance& other);
/// guess log level from a line of game log
virtual MessageLevel::Enum guessLevel([[maybe_unused]] const QString& line, MessageLevel::Enum level) { return level; }
virtual QStringList extraArguments(); virtual QStringList extraArguments();
/// Traits. Normally inside the version, depends on instance implementation. /// Traits. Normally inside the version, depends on instance implementation.
@ -198,15 +195,10 @@ class BaseInstance : public QObject, public std::enable_shared_from_this<BaseIns
virtual QProcessEnvironment createEnvironment() = 0; virtual QProcessEnvironment createEnvironment() = 0;
virtual QProcessEnvironment createLaunchEnvironment() = 0; virtual QProcessEnvironment createLaunchEnvironment() = 0;
/*!
* Returns a matcher that can maps relative paths within the instance to whether they are 'log files'
*/
virtual IPathMatcher::Ptr getLogFileMatcher() = 0;
/*! /*!
* Returns the root folder to use for looking up log files * Returns the root folder to use for looking up log files
*/ */
virtual QString getLogFileRoot() = 0; virtual QStringList getLogFileSearchPaths() = 0;
virtual QString getStatusbarDescription() = 0; virtual QString getStatusbarDescription() = 0;

View file

@ -99,7 +99,7 @@ set(CORE_SOURCES
MTPixmapCache.h MTPixmapCache.h
) )
if (UNIX AND NOT CYGWIN AND NOT APPLE) if (UNIX AND NOT CYGWIN AND NOT APPLE)
set(CORE_SOURCES set(CORE_SOURCES
${CORE_SOURCES} ${CORE_SOURCES}
# MangoHud # MangoHud
@ -175,6 +175,8 @@ set(LAUNCH_SOURCES
launch/LogModel.h launch/LogModel.h
launch/TaskStepWrapper.cpp launch/TaskStepWrapper.cpp
launch/TaskStepWrapper.h launch/TaskStepWrapper.h
logs/LogParser.cpp
logs/LogParser.h
) )
# Old update system # Old update system
@ -308,6 +310,8 @@ set(MINECRAFT_SOURCES
minecraft/ParseUtils.h minecraft/ParseUtils.h
minecraft/ProfileUtils.cpp minecraft/ProfileUtils.cpp
minecraft/ProfileUtils.h minecraft/ProfileUtils.h
minecraft/ShortcutUtils.cpp
minecraft/ShortcutUtils.h
minecraft/Library.cpp minecraft/Library.cpp
minecraft/Library.h minecraft/Library.h
minecraft/MojangDownloadInfo.h minecraft/MojangDownloadInfo.h
@ -589,8 +593,8 @@ set(ATLAUNCHER_SOURCES
) )
set(LINKEXE_SOURCES set(LINKEXE_SOURCES
WindowsConsole.cpp console/WindowsConsole.h
WindowsConsole.h console/WindowsConsole.cpp
filelink/FileLink.h filelink/FileLink.h
filelink/FileLink.cpp filelink/FileLink.cpp
@ -659,6 +663,14 @@ set(PRISMUPDATER_SOURCES
) )
if(WIN32)
set(PRISMUPDATER_SOURCES
console/WindowsConsole.h
console/WindowsConsole.cpp
${PRISMUPDATER_SOURCES}
)
endif()
######## Logging categories ######## ######## Logging categories ########
ecm_qt_declare_logging_category(CORE_SOURCES ecm_qt_declare_logging_category(CORE_SOURCES
@ -786,6 +798,9 @@ SET(LAUNCHER_SOURCES
SysInfo.h SysInfo.h
SysInfo.cpp SysInfo.cpp
# console utils
console/Console.h
# GUI - general utilities # GUI - general utilities
DesktopServices.h DesktopServices.h
DesktopServices.cpp DesktopServices.cpp
@ -821,6 +836,10 @@ SET(LAUNCHER_SOURCES
icons/IconList.h icons/IconList.h
icons/IconList.cpp icons/IconList.cpp
# log utils
logs/AnonymizeLog.cpp
logs/AnonymizeLog.h
# GUI - windows # GUI - windows
ui/GuiUtil.h ui/GuiUtil.h
ui/GuiUtil.cpp ui/GuiUtil.cpp
@ -1031,6 +1050,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ProfileSetupDialog.h ui/dialogs/ProfileSetupDialog.h
ui/dialogs/CopyInstanceDialog.cpp ui/dialogs/CopyInstanceDialog.cpp
ui/dialogs/CopyInstanceDialog.h ui/dialogs/CopyInstanceDialog.h
ui/dialogs/CreateShortcutDialog.cpp
ui/dialogs/CreateShortcutDialog.h
ui/dialogs/CustomMessageBox.cpp ui/dialogs/CustomMessageBox.cpp
ui/dialogs/CustomMessageBox.h ui/dialogs/CustomMessageBox.h
ui/dialogs/ExportInstanceDialog.cpp ui/dialogs/ExportInstanceDialog.cpp
@ -1154,7 +1175,7 @@ SET(LAUNCHER_SOURCES
) )
if (NOT Apple) if (NOT Apple)
set(LAUNCHER_SOURCES set(LAUNCHER_SOURCES
${LAUNCHER_SOURCES} ${LAUNCHER_SOURCES}
ui/dialogs/UpdateAvailableDialog.h ui/dialogs/UpdateAvailableDialog.h
@ -1164,8 +1185,8 @@ endif()
if(WIN32) if(WIN32)
set(LAUNCHER_SOURCES set(LAUNCHER_SOURCES
WindowsConsole.cpp console/WindowsConsole.h
WindowsConsole.h console/WindowsConsole.cpp
${LAUNCHER_SOURCES} ${LAUNCHER_SOURCES}
) )
endif() endif()
@ -1212,6 +1233,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/widgets/MinecraftSettingsWidget.ui ui/widgets/MinecraftSettingsWidget.ui
ui/widgets/JavaSettingsWidget.ui ui/widgets/JavaSettingsWidget.ui
ui/dialogs/CopyInstanceDialog.ui ui/dialogs/CopyInstanceDialog.ui
ui/dialogs/CreateShortcutDialog.ui
ui/dialogs/ProfileSetupDialog.ui ui/dialogs/ProfileSetupDialog.ui
ui/dialogs/ProgressDialog.ui ui/dialogs/ProgressDialog.ui
ui/dialogs/NewInstanceDialog.ui ui/dialogs/NewInstanceDialog.ui
@ -1288,6 +1310,7 @@ target_link_libraries(Launcher_logic
qdcss qdcss
BuildConfig BuildConfig
Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Widgets
qrcode
) )
if (UNIX AND NOT CYGWIN AND NOT APPLE) if (UNIX AND NOT CYGWIN AND NOT APPLE)
@ -1323,11 +1346,11 @@ if(APPLE)
set(CMAKE_INSTALL_RPATH "@loader_path/../Frameworks/") set(CMAKE_INSTALL_RPATH "@loader_path/../Frameworks/")
if(Launcher_ENABLE_UPDATER) if(Launcher_ENABLE_UPDATER)
file(DOWNLOAD ${MACOSX_SPARKLE_DOWNLOAD_URL} ${CMAKE_BINARY_DIR}/Sparkle.tar.xz EXPECTED_HASH SHA256=${MACOSX_SPARKLE_SHA256}) file(DOWNLOAD ${MACOSX_SPARKLE_DOWNLOAD_URL} ${CMAKE_BINARY_DIR}/Sparkle.tar.xz EXPECTED_HASH SHA256=${MACOSX_SPARKLE_SHA256})
file(ARCHIVE_EXTRACT INPUT ${CMAKE_BINARY_DIR}/Sparkle.tar.xz DESTINATION ${CMAKE_BINARY_DIR}/frameworks/Sparkle) file(ARCHIVE_EXTRACT INPUT ${CMAKE_BINARY_DIR}/Sparkle.tar.xz DESTINATION ${CMAKE_BINARY_DIR}/frameworks/Sparkle)
find_library(SPARKLE_FRAMEWORK Sparkle "${CMAKE_BINARY_DIR}/frameworks/Sparkle") find_library(SPARKLE_FRAMEWORK Sparkle "${CMAKE_BINARY_DIR}/frameworks/Sparkle")
add_compile_definitions(SPARKLE_ENABLED) add_compile_definitions(SPARKLE_ENABLED)
endif() endif()
target_link_libraries(Launcher_logic target_link_libraries(Launcher_logic
@ -1337,7 +1360,7 @@ if(APPLE)
"-framework ApplicationServices" "-framework ApplicationServices"
) )
if(Launcher_ENABLE_UPDATER) if(Launcher_ENABLE_UPDATER)
target_link_libraries(Launcher_logic ${SPARKLE_FRAMEWORK}) target_link_libraries(Launcher_logic ${SPARKLE_FRAMEWORK})
endif() endif()
endif() endif()

View file

@ -37,11 +37,7 @@ void DataMigrationTask::dryRunFinished()
disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::dryRunFinished); disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::dryRunFinished);
disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::dryRunAborted); disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::dryRunAborted);
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
if (!m_copyFuture.isValid() || !m_copyFuture.result()) { if (!m_copyFuture.isValid() || !m_copyFuture.result()) {
#else
if (!m_copyFuture.result()) {
#endif
emitFailed(tr("Failed to scan source path.")); emitFailed(tr("Failed to scan source path."));
return; return;
} }
@ -75,11 +71,7 @@ void DataMigrationTask::copyFinished()
disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::copyFinished); disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::copyFinished);
disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::copyAborted); disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::copyAborted);
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
if (!m_copyFuture.isValid() || !m_copyFuture.result()) { if (!m_copyFuture.isValid() || !m_copyFuture.result()) {
#else
if (!m_copyFuture.result()) {
#endif
emitFailed(tr("Some paths could not be copied!")); emitFailed(tr("Some paths could not be copied!"));
return; return;
} }

View file

@ -269,9 +269,9 @@ bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const
return m_ignoreFiles.contains(fileInfo.fileName()) || m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath())); return m_ignoreFiles.contains(fileInfo.fileName()) || m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath()));
} }
bool FileIgnoreProxy::filterFile(const QString& fileName) const bool FileIgnoreProxy::filterFile(const QFileInfo& file) const
{ {
return m_blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(m_root), fileName)); return m_blocked.covers(relPath(file.absoluteFilePath())) || ignoreFile(file);
} }
void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName) void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName)
@ -282,11 +282,7 @@ void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName)
} }
auto ignoreData = ignoreFile.readAll(); auto ignoreData = ignoreFile.readAll();
auto string = QString::fromUtf8(ignoreData); auto string = QString::fromUtf8(ignoreData);
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
setBlockedPaths(string.split('\n', Qt::SkipEmptyParts)); setBlockedPaths(string.split('\n', Qt::SkipEmptyParts));
#else
setBlockedPaths(string.split('\n', QString::SkipEmptyParts));
#endif
} }
void FileIgnoreProxy::saveBlockedPathsToFile(const QString& fileName) void FileIgnoreProxy::saveBlockedPathsToFile(const QString& fileName)

View file

@ -69,7 +69,7 @@ class FileIgnoreProxy : public QSortFilterProxyModel {
// list of relative paths that need to be removed completely from model // list of relative paths that need to be removed completely from model
inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; } inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; }
bool filterFile(const QString& fileName) const; bool filterFile(const QFileInfo& fileName) const;
void loadBlockedPathsFromFile(const QString& fileName); void loadBlockedPathsFromFile(const QString& fileName);

View file

@ -107,6 +107,10 @@ namespace fs = std::filesystem;
#if defined(__MINGW32__) #if defined(__MINGW32__)
// Avoid re-defining structs retroactively added to MinGW
// https://github.com/mingw-w64/mingw-w64/issues/90#issuecomment-2829284729
#if __MINGW64_VERSION_MAJOR < 13
struct _DUPLICATE_EXTENTS_DATA { struct _DUPLICATE_EXTENTS_DATA {
HANDLE FileHandle; HANDLE FileHandle;
LARGE_INTEGER SourceFileOffset; LARGE_INTEGER SourceFileOffset;
@ -116,6 +120,7 @@ struct _DUPLICATE_EXTENTS_DATA {
using DUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA; using DUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA;
using PDUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA*; using PDUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA*;
#endif
struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER { struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER {
WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32
@ -679,9 +684,6 @@ bool deletePath(QString path)
bool trash(QString path, QString* pathInTrash) 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 // FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal
if (DesktopServices::isFlatpak()) if (DesktopServices::isFlatpak())
return false; return false;
@ -690,7 +692,6 @@ bool trash(QString path, QString* pathInTrash)
return false; return false;
#endif #endif
return QFile::moveToTrash(path, pathInTrash); return QFile::moveToTrash(path, pathInTrash);
#endif
} }
QString PathCombine(const QString& path1, const QString& path2) QString PathCombine(const QString& path1, const QString& path2)
@ -724,11 +725,7 @@ int pathDepth(const QString& path)
QFileInfo info(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); auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), Qt::SkipEmptyParts);
#endif
int numParts = parts.length(); int numParts = parts.length();
numParts -= parts.count("."); numParts -= parts.count(".");
@ -748,11 +745,7 @@ QString pathTruncate(const QString& path, int depth)
return pathTruncate(trunc, 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); auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), Qt::SkipEmptyParts);
#endif
if (parts.startsWith(".") && !path.startsWith(".")) { if (parts.startsWith(".") && !path.startsWith(".")) {
parts.removeFirst(); parts.removeFirst();
@ -899,6 +892,11 @@ QString getDesktopDir()
return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
} }
QString getApplicationsDir()
{
return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
}
// Cross-platform Shortcut creation // Cross-platform Shortcut creation
bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon)
{ {
@ -910,16 +908,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
return false; return false;
} }
#if defined(Q_OS_MACOS) #if defined(Q_OS_MACOS)
// Create the Application QDir application = destination + ".app/";
QDir applicationDirectory =
QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + "/" + BuildConfig.LAUNCHER_NAME + " Instances/";
if (!applicationDirectory.mkpath(".")) {
qWarning() << "Couldn't create application directory";
return false;
}
QDir application = applicationDirectory.path() + "/" + name + ".app/";
if (application.exists()) { if (application.exists()) {
qWarning() << "Application already exists!"; qWarning() << "Application already exists!";

View file

@ -353,6 +353,9 @@ bool checkProblemticPathJava(QDir folder);
// Get the Directory representing the User's Desktop // Get the Directory representing the User's Desktop
QString getDesktopDir(); QString getDesktopDir();
// Get the Directory representing the User's Applications directory
QString getApplicationsDir();
// Overrides one folder with the contents of another, preserving items exclusive to the first folder // Overrides one folder with the contents of another, preserving items exclusive to the first folder
// Equivalent to doing QDir::rename, but allowing for overrides // Equivalent to doing QDir::rename, but allowing for overrides
bool overrideFolder(QString overwritten_path, QString override_path); bool overrideFolder(QString overwritten_path, QString override_path);

View file

@ -36,6 +36,8 @@
#include "GZip.h" #include "GZip.h"
#include <zlib.h> #include <zlib.h>
#include <QByteArray> #include <QByteArray>
#include <QDebug>
#include <QFile>
bool GZip::unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes) bool GZip::unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes)
{ {
@ -136,3 +138,81 @@ bool GZip::zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes)
} }
return true; return true;
} }
int inf(QFile* source, std::function<bool(const QByteArray&)> 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<Bytef*>(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<const char*>(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<bool(const QByteArray&)> handleBlock)
{
auto ret = inf(source, handleBlock);
return zerr(ret);
}

View file

@ -1,8 +1,11 @@
#pragma once #pragma once
#include <QByteArray> #include <QByteArray>
#include <QFile>
class GZip { namespace GZip {
public:
static bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes); bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes);
static bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes); bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes);
}; QString readGzFileByBlocks(QFile* source, std::function<bool(const QByteArray&)> handleBlock);
} // namespace GZip

View file

@ -72,7 +72,6 @@ bool InstanceImportTask::abort()
bool wasAborted = false; bool wasAborted = false;
if (m_task) if (m_task)
wasAborted = m_task->abort(); wasAborted = m_task->abort();
Task::abort();
return wasAborted; return wasAborted;
} }
@ -263,6 +262,25 @@ void InstanceImportTask::extractFinished()
} }
} }
bool installIcon(QString root, QString instIcon)
{
auto importIconPath = IconUtils::findBestIconIn(root, instIcon);
if (importIconPath.isNull() || !QFile::exists(importIconPath))
importIconPath = IconUtils::findBestIconIn(root, "icon.png");
if (importIconPath.isNull() || !QFile::exists(importIconPath))
importIconPath = IconUtils::findBestIconIn(FS::PathCombine(root, "overrides"), "icon.png");
if (!importIconPath.isNull() && QFile::exists(importIconPath)) {
// import icon
auto iconList = APPLICATION->icons();
if (iconList->iconFileExists(instIcon)) {
iconList->deleteIcon(instIcon);
}
iconList->installIcon(importIconPath, instIcon);
return true;
}
return false;
}
void InstanceImportTask::processFlame() void InstanceImportTask::processFlame()
{ {
shared_qobject_ptr<FlameCreationTask> inst_creation_task = nullptr; shared_qobject_ptr<FlameCreationTask> inst_creation_task = nullptr;
@ -288,6 +306,14 @@ void InstanceImportTask::processFlame()
} }
inst_creation_task->setName(*this); inst_creation_task->setName(*this);
// if the icon was specified by user, use that. otherwise pull icon from the pack
if (m_instIcon == "default") {
auto iconKey = QString("Flame_%1_Icon").arg(name());
if (installIcon(m_stagingPath, iconKey)) {
m_instIcon = iconKey;
}
}
inst_creation_task->setIcon(m_instIcon); inst_creation_task->setIcon(m_instIcon);
inst_creation_task->setGroup(m_instGroup); inst_creation_task->setGroup(m_instGroup);
inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate());
@ -305,7 +331,7 @@ void InstanceImportTask::processFlame()
connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails);
connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted);
connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable);
m_task.reset(inst_creation_task); m_task.reset(inst_creation_task);
@ -340,17 +366,7 @@ void InstanceImportTask::processMultiMC()
} else { } else {
m_instIcon = instance.iconKey(); m_instIcon = instance.iconKey();
auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon); installIcon(instance.instanceRoot(), m_instIcon);
if (importIconPath.isNull() || !QFile::exists(importIconPath))
importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), "icon.png");
if (!importIconPath.isNull() && QFile::exists(importIconPath)) {
// import icon
auto iconList = APPLICATION->icons();
if (iconList->iconFileExists(m_instIcon)) {
iconList->deleteIcon(m_instIcon);
}
iconList->installIcon(importIconPath, m_instIcon);
}
} }
emitSucceeded(); emitSucceeded();
} }
@ -378,8 +394,8 @@ void InstanceImportTask::processModrinth()
} else { } else {
QString pack_id; QString pack_id;
if (!m_sourceUrl.isEmpty()) { if (!m_sourceUrl.isEmpty()) {
QRegularExpression regex(R"(data\/([^\/]*)\/versions)"); static const QRegularExpression s_regex(R"(data\/([^\/]*)\/versions)");
pack_id = regex.match(m_sourceUrl.toString()).captured(1); pack_id = s_regex.match(m_sourceUrl.toString()).captured(1);
} }
// FIXME: Find a way to get the ID in directly imported ZIPs // FIXME: Find a way to get the ID in directly imported ZIPs
@ -387,6 +403,14 @@ void InstanceImportTask::processModrinth()
} }
inst_creation_task->setName(*this); inst_creation_task->setName(*this);
// if the icon was specified by user, use that. otherwise pull icon from the pack
if (m_instIcon == "default") {
auto iconKey = QString("Modrinth_%1_Icon").arg(name());
if (installIcon(m_stagingPath, iconKey)) {
m_instIcon = iconKey;
}
}
inst_creation_task->setIcon(m_instIcon); inst_creation_task->setIcon(m_instIcon);
inst_creation_task->setGroup(m_instGroup); inst_creation_task->setGroup(m_instGroup);
inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate());
@ -404,7 +428,7 @@ void InstanceImportTask::processModrinth()
connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails);
connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted);
connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable);
m_task.reset(inst_creation_task); m_task.reset(inst_creation_task);

View file

@ -428,7 +428,7 @@ static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr>&
QList<InstanceId> InstanceList::discoverInstances() QList<InstanceId> InstanceList::discoverInstances()
{ {
qDebug() << "Discovering instances in" << m_instDir; qInfo() << "Discovering instances in" << m_instDir;
QList<InstanceId> out; QList<InstanceId> out;
QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks);
while (iter.hasNext()) { while (iter.hasNext()) {
@ -447,13 +447,9 @@ QList<InstanceId> InstanceList::discoverInstances()
} }
auto id = dirInfo.fileName(); auto id = dirInfo.fileName();
out.append(id); out.append(id);
qDebug() << "Found instance ID" << id; qInfo() << "Found instance ID" << id;
} }
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
instanceSet = QSet<QString>(out.begin(), out.end()); instanceSet = QSet<QString>(out.begin(), out.end());
#else
instanceSet = out.toSet();
#endif
m_instancesProbed = true; m_instancesProbed = true;
return out; return out;
} }
@ -468,7 +464,7 @@ InstanceList::InstListError InstanceList::loadList()
if (existingIds.contains(id)) { if (existingIds.contains(id)) {
auto instPair = existingIds[id]; auto instPair = existingIds[id];
existingIds.remove(id); existingIds.remove(id);
qDebug() << "Should keep and soft-reload" << id; qInfo() << "Should keep and soft-reload" << id;
} else { } else {
InstancePtr instPtr = loadInstance(id); InstancePtr instPtr = loadInstance(id);
if (instPtr) { if (instPtr) {

View file

@ -44,10 +44,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider {
// values.append(new GameOptionsPage(onesix.get())); // values.append(new GameOptionsPage(onesix.get()));
values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots")));
values.append(new InstanceSettingsPage(onesix)); values.append(new InstanceSettingsPage(onesix));
auto logMatcher = inst->getLogFileMatcher(); values.append(new OtherLogsPage(inst));
if (logMatcher) {
values.append(new OtherLogsPage(inst->getLogFileRoot(), logMatcher));
}
return values; return values;
} }

View file

@ -41,7 +41,9 @@
bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent) 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")) { jvmargs.contains("-XX:InitialHeapSize")) {
auto warnStr = QObject::tr( auto warnStr = QObject::tr(
"You tried to manually set a JVM memory option (using \"-XX:PermSize\", \"-XX-MaxHeapSize\", \"-XX:InitialHeapSize\", \"-Xmx\" " "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; return false;
} }
// block lunacy with passing required version to the JVM // block lunacy with passing required version to the JVM
if (jvmargs.contains(QRegularExpression("-version:.*"))) { if (jvmargs.contains(s_versionRegex)) {
auto warnStr = QObject::tr( 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 " "You tried to pass required Java version argument to the JVM (using \"-version:xxx\"). This is not safe and will not be "
"allowed.\n" "allowed.\n"

View file

@ -188,10 +188,10 @@ T ensureIsType(const QJsonObject& parent, const QString& key, const T default_ =
} }
template <typename T> template <typename T>
QVector<T> requireIsArrayOf(const QJsonDocument& doc) QList<T> requireIsArrayOf(const QJsonDocument& doc)
{ {
const QJsonArray array = requireArray(doc); const QJsonArray array = requireArray(doc);
QVector<T> out; QList<T> out;
for (const QJsonValue val : array) { for (const QJsonValue val : array) {
out.append(requireIsType<T>(val, "Document")); out.append(requireIsType<T>(val, "Document"));
} }
@ -199,10 +199,10 @@ QVector<T> requireIsArrayOf(const QJsonDocument& doc)
} }
template <typename T> template <typename T>
QVector<T> ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value") QList<T> ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value")
{ {
const QJsonArray array = ensureIsType<QJsonArray>(value, QJsonArray(), what); const QJsonArray array = ensureIsType<QJsonArray>(value, QJsonArray(), what);
QVector<T> out; QList<T> out;
for (const QJsonValue val : array) { for (const QJsonValue val : array) {
out.append(requireIsType<T>(val, what)); out.append(requireIsType<T>(val, what));
} }
@ -210,7 +210,7 @@ QVector<T> ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value
} }
template <typename T> template <typename T>
QVector<T> ensureIsArrayOf(const QJsonValue& value, const QVector<T> default_, const QString& what = "Value") QList<T> ensureIsArrayOf(const QJsonValue& value, const QList<T> default_, const QString& what = "Value")
{ {
if (value.isUndefined()) { if (value.isUndefined()) {
return default_; return default_;
@ -220,7 +220,7 @@ QVector<T> ensureIsArrayOf(const QJsonValue& value, const QVector<T> default_, c
/// @throw JsonException /// @throw JsonException
template <typename T> template <typename T>
QVector<T> requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") QList<T> requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__")
{ {
const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
if (!parent.contains(key)) { if (!parent.contains(key)) {
@ -230,10 +230,10 @@ QVector<T> requireIsArrayOf(const QJsonObject& parent, const QString& key, const
} }
template <typename T> template <typename T>
QVector<T> ensureIsArrayOf(const QJsonObject& parent, QList<T> ensureIsArrayOf(const QJsonObject& parent,
const QString& key, const QString& key,
const QVector<T>& default_ = QVector<T>(), const QList<T>& default_ = QList<T>(),
const QString& what = "__placeholder__") const QString& what = "__placeholder__")
{ {
const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
if (!parent.contains(key)) { if (!parent.contains(key)) {

View file

@ -182,7 +182,8 @@ void LaunchController::login()
auto name = askOfflineName("Player", m_demo, ok); auto name = askOfflineName("Player", m_demo, ok);
if (ok) { if (ok) {
m_session = std::make_shared<AuthSession>(); m_session = std::make_shared<AuthSession>();
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(); launchInstance();
return; return;
} }

View file

@ -418,7 +418,7 @@ bool extractFile(QString fileCompressed, QString file, QString target)
return extractRelFile(&zip, file, target); return extractRelFile(&zip, file, target);
} }
bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter) bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter)
{ {
QDir rootDirectory(rootDir); QDir rootDirectory(rootDir);
if (!rootDirectory.exists()) if (!rootDirectory.exists())
@ -443,8 +443,8 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q
// collect files // collect files
entries = directory.entryInfoList(QDir::Files); entries = directory.entryInfoList(QDir::Files);
for (const auto& e : entries) { for (const auto& e : entries) {
QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); if (excludeFilter && excludeFilter(e)) {
if (excludeFilter && excludeFilter(relativeFilePath)) { QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath());
qDebug() << "Skipping file " << relativeFilePath; qDebug() << "Skipping file " << relativeFilePath;
continue; continue;
} }

View file

@ -56,6 +56,7 @@
namespace MMCZip { namespace MMCZip {
using FilterFunction = std::function<bool(const QString&)>; using FilterFunction = std::function<bool(const QString&)>;
using FilterFileFunction = std::function<bool(const QFileInfo&)>;
/** /**
* Merge two zip files, using a filter 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) * \param excludeFilter function to excludeFilter which files shouldn't be included (returning true means to excude)
* \return true for success or false for failure * \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) #if defined(LAUNCHER_APPLICATION)
class ExportToZipTask : public Task { class ExportToZipTask : public Task {

View file

@ -2,19 +2,22 @@
MessageLevel::Enum MessageLevel::getLevel(const QString& levelName) MessageLevel::Enum MessageLevel::getLevel(const QString& levelName)
{ {
if (levelName == "Launcher") QString name = levelName.toUpper();
if (name == "LAUNCHER")
return MessageLevel::Launcher; return MessageLevel::Launcher;
else if (levelName == "Debug") else if (name == "TRACE")
return MessageLevel::Trace;
else if (name == "DEBUG")
return MessageLevel::Debug; return MessageLevel::Debug;
else if (levelName == "Info") else if (name == "INFO")
return MessageLevel::Info; return MessageLevel::Info;
else if (levelName == "Message") else if (name == "MESSAGE")
return MessageLevel::Message; return MessageLevel::Message;
else if (levelName == "Warning") else if (name == "WARNING" || name == "WARN")
return MessageLevel::Warning; return MessageLevel::Warning;
else if (levelName == "Error") else if (name == "ERROR")
return MessageLevel::Error; return MessageLevel::Error;
else if (levelName == "Fatal") else if (name == "FATAL")
return MessageLevel::Fatal; return MessageLevel::Fatal;
// Skip PrePost, it's not exposed to !![]! // Skip PrePost, it's not exposed to !![]!
// Also skip StdErr and StdOut // Also skip StdErr and StdOut

View file

@ -12,6 +12,7 @@ enum Enum {
StdOut, /**< Undetermined stderr messages */ StdOut, /**< Undetermined stderr messages */
StdErr, /**< Undetermined stdout messages */ StdErr, /**< Undetermined stdout messages */
Launcher, /**< Launcher Messages */ Launcher, /**< Launcher Messages */
Trace, /**< Trace Messages */
Debug, /**< Debug Messages */ Debug, /**< Debug Messages */
Info, /**< Info Messages */ Info, /**< Info Messages */
Message, /**< Standard Messages */ Message, /**< Standard Messages */

View file

@ -57,8 +57,7 @@ class NullInstance : public BaseInstance {
QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); } QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); }
QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); } QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); }
QMap<QString, QString> getVariables() override { return QMap<QString, QString>(); } QMap<QString, QString> getVariables() override { return QMap<QString, QString>(); }
IPathMatcher::Ptr getLogFileMatcher() override { return nullptr; } QStringList getLogFileSearchPaths() override { return {}; }
QString getLogFileRoot() override { return instanceRoot(); }
QString typeName() const override { return "Null"; } QString typeName() const override { return "Null"; }
bool canExport() const override { return false; } bool canExport() const override { return false; }
bool canEdit() const override { return false; } bool canEdit() const override { return false; }

View file

@ -1,7 +1,6 @@
#include "RecursiveFileSystemWatcher.h" #include "RecursiveFileSystemWatcher.h"
#include <QDebug> #include <QDebug>
#include <QRegularExpression>
RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject* parent) : QObject(parent), m_watcher(new QFileSystemWatcher(this)) RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject* parent) : QObject(parent), m_watcher(new QFileSystemWatcher(this))
{ {

View file

@ -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 StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs)
{ {
int l1 = 0, l2 = 0; 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 // skip spaces, tabs and 0's
QChar c1 = getNextChar(s1, l1); QChar c1 = getNextChar(s1, l1);
while (c1.isSpace()) while (c1.isSpace())
@ -213,11 +213,10 @@ QPair<QString, QString> StringUtils::splitFirst(const QString& s, const QRegular
return qMakePair(left, right); return qMakePair(left, right);
} }
static const QRegularExpression ulMatcher("<\\s*/\\s*ul\\s*>");
QString StringUtils::htmlListPatch(QString htmlStr) 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; int imgPos;
while (pos != -1) { while (pos != -1) {
pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the </ul> tag. Add one for zeroeth index pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the </ul> tag. Add one for zeroeth index
@ -230,7 +229,7 @@ QString StringUtils::htmlListPatch(QString htmlStr)
if (textBetween.isEmpty()) if (textBetween.isEmpty())
htmlStr.insert(pos, "<br>"); htmlStr.insert(pos, "<br>");
pos = htmlStr.indexOf(ulMatcher, pos); pos = htmlStr.indexOf(s_ulMatcher, pos);
} }
return htmlStr; return htmlStr;
} }

View file

@ -1,7 +1,6 @@
#include "Version.h" #include "Version.h"
#include <QDebug> #include <QDebug>
#include <QRegularExpression>
#include <QRegularExpressionMatch> #include <QRegularExpressionMatch>
#include <QUrl> #include <QUrl>

View file

@ -72,22 +72,14 @@ class Version {
} }
} }
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
auto numPart = QStringView{ m_fullString }.left(cutoff); auto numPart = QStringView{ m_fullString }.left(cutoff);
#else
auto numPart = m_fullString.leftRef(cutoff);
#endif
if (!numPart.isEmpty()) { if (!numPart.isEmpty()) {
m_isNull = false; m_isNull = false;
m_numPart = numPart.toInt(); m_numPart = numPart.toInt();
} }
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
auto stringPart = QStringView{ m_fullString }.mid(cutoff); auto stringPart = QStringView{ m_fullString }.mid(cutoff);
#else
auto stringPart = m_fullString.midRef(cutoff);
#endif
if (!stringPart.isEmpty()) { if (!stringPart.isEmpty()) {
m_isNull = false; m_isNull = false;

View file

@ -295,13 +295,11 @@ void VersionProxyModel::sourceDataChanged(const QModelIndex& source_top_left, co
void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw) void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw)
{ {
auto replacing = dynamic_cast<BaseVersionList*>(replacingRaw); auto replacing = dynamic_cast<BaseVersionList*>(replacingRaw);
beginResetModel();
m_columns.clear(); m_columns.clear();
if (!replacing) { if (!replacing) {
roles.clear(); roles.clear();
filterModel->setSourceModel(replacing); filterModel->setSourceModel(replacing);
endResetModel();
return; return;
} }
@ -343,8 +341,6 @@ void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw)
hasLatest = true; hasLatest = true;
} }
filterModel->setSourceModel(replacing); filterModel->setSourceModel(replacing);
endResetModel();
} }
QModelIndex VersionProxyModel::getRecommended() const QModelIndex VersionProxyModel::getRecommended() const

View file

@ -0,0 +1,33 @@
#pragma once
#include <QString>
#include <ostream>
#if defined Q_OS_WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <unistd.h>
#include <cstdio>
#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

View file

@ -16,13 +16,18 @@
* *
*/ */
#include "WindowsConsole.h"
#include <system_error>
#ifndef WIN32_LEAN_AND_MEAN #ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#endif #endif
#include <windows.h>
#include <fcntl.h> #include <fcntl.h>
#include <io.h> #include <io.h>
#include <stdio.h> #include <stdio.h>
#include <windows.h> #include <cstddef>
#include <iostream> #include <iostream>
void RedirectHandle(DWORD handle, FILE* stream, const char* mode) void RedirectHandle(DWORD handle, FILE* stream, const char* mode)
@ -126,3 +131,29 @@ bool AttachWindowsConsole()
return false; 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 {};
}

View file

@ -21,5 +21,8 @@
#pragma once #pragma once
#include <system_error>
void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr); void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr);
bool AttachWindowsConsole(); bool AttachWindowsConsole();
std::error_code EnableAnsiSupport();

View file

@ -37,7 +37,10 @@
#include <sys.h> #include <sys.h>
#if defined Q_OS_WIN32 #if defined Q_OS_WIN32
#include "WindowsConsole.h" #ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include "console/WindowsConsole.h"
#endif #endif
#include <filesystem> #include <filesystem>

View file

@ -137,11 +137,7 @@ QString formatName(const QDir& iconsDir, const QFileInfo& iconFile)
/// Split into a separate function because the preprocessing impedes readability /// Split into a separate function because the preprocessing impedes readability
QSet<QString> toStringSet(const QList<QString>& list) QSet<QString> toStringSet(const QList<QString>& list)
{ {
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QSet<QString> set(list.begin(), list.end()); QSet<QString> set(list.begin(), list.end());
#else
QSet<QString> set = list.toSet();
#endif
return set; return set;
} }
@ -165,7 +161,8 @@ void IconList::directoryChanged(const QString& path)
for (const MMCIcon& it : m_icons) { for (const MMCIcon& it : m_icons) {
if (!it.has(IconType::FileBased)) if (!it.has(IconType::FileBased))
continue; continue;
currentSet.insert(it.m_images[IconType::FileBased].filename); QFileInfo icon(it.getFilePath());
currentSet.insert(icon.absoluteFilePath());
} }
QSet<QString> toRemove = currentSet - newSet; QSet<QString> toRemove = currentSet - newSet;
QSet<QString> toAdd = newSet - currentSet; QSet<QString> toAdd = newSet - currentSet;
@ -173,7 +170,8 @@ void IconList::directoryChanged(const QString& path)
for (const QString& removedPath : toRemove) { for (const QString& removedPath : toRemove) {
qDebug() << "Removing icon " << removedPath; qDebug() << "Removing icon " << removedPath;
QFileInfo removedFile(removedPath); QFileInfo removedFile(removedPath);
QString key = m_dir.relativeFilePath(removedFile.absoluteFilePath()); QString relativePath = m_dir.relativeFilePath(removedFile.absoluteFilePath());
QString key = QFileInfo(relativePath).completeBaseName();
int idx = getIconIndex(key); int idx = getIconIndex(key);
if (idx == -1) if (idx == -1)

View file

@ -106,6 +106,6 @@ class IconList : public QAbstractListModel {
shared_qobject_ptr<QFileSystemWatcher> m_watcher; shared_qobject_ptr<QFileSystemWatcher> m_watcher;
bool m_isWatching; bool m_isWatching;
QMap<QString, int> m_nameIndex; QMap<QString, int> m_nameIndex;
QVector<MMCIcon> m_icons; QList<MMCIcon> m_icons;
QDir m_dir; QDir m_dir;
}; };

View file

@ -137,11 +137,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status)
QMap<QString, QString> results; QMap<QString, QString> results;
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QStringList lines = m_stdout.split("\n", Qt::SkipEmptyParts); QStringList lines = m_stdout.split("\n", Qt::SkipEmptyParts);
#else
QStringList lines = m_stdout.split("\n", QString::SkipEmptyParts);
#endif
for (QString line : lines) { for (QString line : lines) {
line = line.trimmed(); line = line.trimmed();
// NOTE: workaround for GH-4125, where garbage is getting printed into stdout on bedrock linux // 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; continue;
} }
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
auto parts = line.split('=', Qt::SkipEmptyParts); auto parts = line.split('=', Qt::SkipEmptyParts);
#else
auto parts = line.split('=', QString::SkipEmptyParts);
#endif
if (parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) { if (parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) {
continue; continue;
} else { } else {

View file

@ -365,13 +365,13 @@ QList<QString> JavaUtils::FindJavaPaths()
javas.append("/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java"); javas.append("/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java");
QDir libraryJVMDir("/Library/Java/JavaVirtualMachines/"); QDir libraryJVMDir("/Library/Java/JavaVirtualMachines/");
QStringList libraryJVMJavas = libraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); QStringList libraryJVMJavas = libraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
foreach (const QString& java, libraryJVMJavas) { for (const QString& java : libraryJVMJavas) {
javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/jre/bin/java"); javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/jre/bin/java");
} }
QDir systemLibraryJVMDir("/System/Library/Java/JavaVirtualMachines/"); QDir systemLibraryJVMDir("/System/Library/Java/JavaVirtualMachines/");
QStringList systemLibraryJVMJavas = systemLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); QStringList systemLibraryJVMJavas = systemLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
foreach (const QString& java, systemLibraryJVMJavas) { for (const QString& java : systemLibraryJVMJavas) {
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java");
} }
@ -381,14 +381,14 @@ QList<QString> JavaUtils::FindJavaPaths()
// javas downloaded by sdkman // javas downloaded by sdkman
QDir sdkmanDir(FS::PathCombine(home, ".sdkman/candidates/java")); QDir sdkmanDir(FS::PathCombine(home, ".sdkman/candidates/java"));
QStringList sdkmanJavas = sdkmanDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); QStringList sdkmanJavas = sdkmanDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
foreach (const QString& java, sdkmanJavas) { for (const QString& java : sdkmanJavas) {
javas.append(sdkmanDir.absolutePath() + "/" + java + "/bin/java"); javas.append(sdkmanDir.absolutePath() + "/" + java + "/bin/java");
} }
// java in user library folder (like from intellij downloads) // java in user library folder (like from intellij downloads)
QDir userLibraryJVMDir(FS::PathCombine(home, "Library/Java/JavaVirtualMachines/")); QDir userLibraryJVMDir(FS::PathCombine(home, "Library/Java/JavaVirtualMachines/"));
QStringList userLibraryJVMJavas = userLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); QStringList userLibraryJVMJavas = userLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
foreach (const QString& java, userLibraryJVMJavas) { for (const QString& java : userLibraryJVMJavas) {
javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java");
} }

View file

@ -19,9 +19,13 @@ JavaVersion& JavaVersion::operator=(const QString& javaVersionString)
QRegularExpression pattern; QRegularExpression pattern;
if (javaVersionString.startsWith("1.")) { if (javaVersionString.startsWith("1.")) {
pattern = QRegularExpression("1[.](?<major>[0-9]+)([.](?<minor>[0-9]+))?(_(?<security>[0-9]+)?)?(-(?<prerelease>[a-zA-Z0-9]+))?"); static const QRegularExpression s_withOne(
"1[.](?<major>[0-9]+)([.](?<minor>[0-9]+))?(_(?<security>[0-9]+)?)?(-(?<prerelease>[a-zA-Z0-9]+))?");
pattern = s_withOne;
} else { } else {
pattern = QRegularExpression("(?<major>[0-9]+)([.](?<minor>[0-9]+))?([.](?<security>[0-9]+))?(-(?<prerelease>[a-zA-Z0-9]+))?"); static const QRegularExpression s_withoutOne(
"(?<major>[0-9]+)([.](?<minor>[0-9]+))?([.](?<security>[0-9]+))?(-(?<prerelease>[a-zA-Z0-9]+))?");
pattern = s_withoutOne;
} }
auto match = pattern.match(m_string); auto match = pattern.match(m_string);

View file

@ -55,6 +55,7 @@ void ArchiveDownloadTask::executeTask()
connect(download.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); connect(download.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress);
connect(download.get(), &Task::status, this, &ArchiveDownloadTask::setStatus); connect(download.get(), &Task::status, this, &ArchiveDownloadTask::setStatus);
connect(download.get(), &Task::details, this, &ArchiveDownloadTask::setDetails); connect(download.get(), &Task::details, this, &ArchiveDownloadTask::setDetails);
connect(download.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted);
connect(download.get(), &Task::succeeded, [this, fullPath] { connect(download.get(), &Task::succeeded, [this, fullPath] {
// This should do all of the extracting and creating folders // This should do all of the extracting and creating folders
extractJava(fullPath); extractJava(fullPath);
@ -135,7 +136,6 @@ bool ArchiveDownloadTask::abort()
auto aborted = canAbort(); auto aborted = canAbort();
if (m_task) if (m_task)
aborted = m_task->abort(); aborted = m_task->abort();
emitAborted();
return aborted; return aborted;
}; };
} // namespace Java } // namespace Java

View file

@ -37,11 +37,12 @@
#include "launch/LaunchTask.h" #include "launch/LaunchTask.h"
#include <assert.h> #include <assert.h>
#include <QAnyStringView>
#include <QCoreApplication> #include <QCoreApplication>
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
#include <QRegularExpression>
#include <QStandardPaths> #include <QStandardPaths>
#include <variant>
#include "MessageLevel.h" #include "MessageLevel.h"
#include "tasks/Task.h" #include "tasks/Task.h"
@ -213,6 +214,52 @@ shared_qobject_ptr<LogModel> LaunchTask::getLogModel()
return m_logModel; 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<LogParser::LogEntry>(item)) {
auto entry = std::get<LogParser::LogEntry>(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<LogParser::PlainText>(item)) {
auto msg = std::get<LogParser::PlainText>(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) void LaunchTask::onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel)
{ {
for (auto& line : lines) { for (auto& line : lines) {
@ -222,21 +269,26 @@ void LaunchTask::onLogLines(const QStringList& lines, MessageLevel::Enum default
void LaunchTask::onLogLine(QString line, MessageLevel::Enum level) void LaunchTask::onLogLine(QString line, MessageLevel::Enum level)
{ {
if (parseXmlLogs(line, level)) {
return;
}
// if the launcher part set a log level, use it // if the launcher part set a log level, use it
auto innerLevel = MessageLevel::fromLine(line); auto innerLevel = MessageLevel::fromLine(line);
if (innerLevel != MessageLevel::Unknown) { if (innerLevel != MessageLevel::Unknown) {
level = innerLevel; level = innerLevel;
} }
auto& model = *getLogModel();
// If the level is still undetermined, guess level // If the level is still undetermined, guess level
if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) { if (level == MessageLevel::Unknown) {
level = m_instance->guessLevel(line, level); level = LogParser::guessLevel(line, model.previousLevel());
} }
// censor private user info // censor private user info
line = censorPrivateInfo(line); line = censorPrivateInfo(line);
auto& model = *getLogModel();
model.append(level, line); model.append(level, line);
} }

View file

@ -43,6 +43,7 @@
#include "LaunchStep.h" #include "LaunchStep.h"
#include "LogModel.h" #include "LogModel.h"
#include "MessageLevel.h" #include "MessageLevel.h"
#include "logs/LogParser.h"
class LaunchTask : public Task { class LaunchTask : public Task {
Q_OBJECT Q_OBJECT
@ -114,6 +115,9 @@ class LaunchTask : public Task {
private: /*methods */ private: /*methods */
void finalizeSteps(bool successful, const QString& error); void finalizeSteps(bool successful, const QString& error);
protected:
bool parseXmlLogs(QString const& line, MessageLevel::Enum level);
protected: /* data */ protected: /* data */
MinecraftInstancePtr m_instance; MinecraftInstancePtr m_instance;
shared_qobject_ptr<LogModel> m_logModel; shared_qobject_ptr<LogModel> m_logModel;
@ -122,4 +126,6 @@ class LaunchTask : public Task {
int currentStep = -1; int currentStep = -1;
State state = NotStarted; State state = NotStarted;
qint64 m_pid = -1; qint64 m_pid = -1;
LogParser m_stdoutParser;
LogParser m_stderrParser;
}; };

View file

@ -100,7 +100,7 @@ void LogModel::setMaxLines(int maxLines)
return; return;
} }
// otherwise, we need to reorganize the data because it crosses the wrap boundary // otherwise, we need to reorganize the data because it crosses the wrap boundary
QVector<entry> newContent; QList<entry> newContent;
newContent.resize(maxLines); newContent.resize(maxLines);
if (m_numLines <= maxLines) { if (m_numLines <= maxLines) {
// if it all fits in the new buffer, just copy it over // if it all fits in the new buffer, just copy it over
@ -149,3 +149,28 @@ bool LogModel::wrapLines() const
{ {
return m_lineWrap; 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;
}

View file

@ -24,9 +24,14 @@ class LogModel : public QAbstractListModel {
void setMaxLines(int maxLines); void setMaxLines(int maxLines);
void setStopOnOverflow(bool stop); void setStopOnOverflow(bool stop);
void setOverflowMessage(const QString& overflowMessage); void setOverflowMessage(const QString& overflowMessage);
bool isOverFlow();
void setLineWrap(bool state); void setLineWrap(bool state);
bool wrapLines() const; bool wrapLines() const;
void setColorLines(bool state);
bool colorLines() const;
MessageLevel::Enum previousLevel();
enum Roles { LevelRole = Qt::UserRole }; enum Roles { LevelRole = Qt::UserRole };
@ -37,7 +42,7 @@ class LogModel : public QAbstractListModel {
}; };
private: /* data */ private: /* data */
QVector<entry> m_content; QList<entry> m_content;
int m_maxLines = 1000; int m_maxLines = 1000;
// first line in the circular buffer // first line in the circular buffer
int m_firstLine = 0; int m_firstLine = 0;
@ -47,6 +52,7 @@ class LogModel : public QAbstractListModel {
QString m_overflowMessage = "OVERFLOW"; QString m_overflowMessage = "OVERFLOW";
bool m_suspended = false; bool m_suspended = false;
bool m_lineWrap = true; bool m_lineWrap = true;
bool m_colorLines = true;
private: private:
Q_DISABLE_COPY(LogModel) Q_DISABLE_COPY(LogModel)

View file

@ -49,14 +49,10 @@ void PostLaunchCommand::executeTask()
{ {
auto cmd = m_parent->substituteVariables(m_command); auto cmd = m_parent->substituteVariables(m_command);
emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher); emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
auto args = QProcess::splitCommand(cmd); auto args = QProcess::splitCommand(cmd);
const QString program = args.takeFirst(); const QString program = args.takeFirst();
m_process.start(program, args); m_process.start(program, args);
#else
m_process.start(cmd);
#endif
} }
void PostLaunchCommand::on_state(LoggedProcess::State state) void PostLaunchCommand::on_state(LoggedProcess::State state)

View file

@ -49,13 +49,9 @@ void PreLaunchCommand::executeTask()
{ {
auto cmd = m_parent->substituteVariables(m_command); auto cmd = m_parent->substituteVariables(m_command);
emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher); emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
auto args = QProcess::splitCommand(cmd); auto args = QProcess::splitCommand(cmd);
const QString program = args.takeFirst(); const QString program = args.takeFirst();
m_process.start(program, args); m_process.start(program, args);
#else
m_process.start(cmd);
#endif
} }
void PreLaunchCommand::on_state(LoggedProcess::State state) void PreLaunchCommand::on_state(LoggedProcess::State state)

View file

@ -0,0 +1,68 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "AnonymizeLog.h"
#include <QRegularExpression>
struct RegReplace {
RegReplace(QRegularExpression r, QString w) : reg(r), with(w) { reg.optimize(); }
QRegularExpression reg;
QString with;
};
static const QVector<RegReplace> anonymizeRules = {
RegReplace(QRegularExpression("C:\\\\Users\\\\([^\\\\]+)\\\\", QRegularExpression::CaseInsensitiveOption),
"C:\\Users\\********\\"), // windows
RegReplace(QRegularExpression("C:\\/Users\\/([^\\/]+)\\/", QRegularExpression::CaseInsensitiveOption),
"C:/Users/********/"), // windows with forward slashes
RegReplace(QRegularExpression("(?<!\\\\w)\\/home\\/[^\\/]+\\/", QRegularExpression::CaseInsensitiveOption),
"/home/********/"), // linux
RegReplace(QRegularExpression("(?<!\\\\w)\\/Users\\/[^\\/]+\\/", QRegularExpression::CaseInsensitiveOption),
"/Users/********/"), // macos
RegReplace(QRegularExpression("\\(Session ID is [^\\)]+\\)", QRegularExpression::CaseInsensitiveOption),
"(Session ID is <SESSION_TOKEN>)"), // SESSION_TOKEN
RegReplace(QRegularExpression("new refresh token: \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption),
"new refresh token: \"<TOKEN>\""), // refresh token
RegReplace(QRegularExpression("\"device_code\" : \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption),
"\"device_code\" : \"<DEVICE_CODE>\""), // device code
};
void anonymizeLog(QString& log)
{
for (auto rule : anonymizeRules) {
log.replace(rule.reg, rule.with);
}
}

View file

@ -0,0 +1,40 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QString>
void anonymizeLog(QString& log);

351
launcher/logs/LogParser.cpp Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*
*/
#include "LogParser.h"
#include <QRegularExpression>
#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::Error> LogParser::getError()
{
return m_error;
}
std::optional<LogParser::LogEntry> 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("<log4j:event");
if (buffer.isEmpty() || buffer[0] != '<') {
return false;
}
auto bufLower = buffer.toString().toLower();
return target.startsWith(bufLower) || bufLower.startsWith(target);
}
std::optional<LogParser::ParsedItem> 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::ParsedItem> LogParser::parseAvailable()
{
QList<LogParser::ParsedItem> 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<LogParser::Partial>(item)) {
break;
} else {
items.push_back(item);
}
} else {
doNext = false;
}
}
return items;
}
std::optional<LogParser::ParsedItem> 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("^\\[(?<timestamp>[0-9:]+)\\] \\[[^/]+/(?<level>[^\\]]+)\\]");
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;
}

76
launcher/logs/LogParser.h Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*
*/
#include <QAnyStringView>
#include <QDateTime>
#include <QList>
#include <QString>
#include <QStringView>
#include <QXmlStreamReader>
#include <optional>
#include <variant>
#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<LogEntry, PlainText, Partial>;
public:
LogParser() = default;
void appendLine(QAnyStringView data);
std::optional<ParsedItem> parseNext();
QList<ParsedItem> parseAvailable();
std::optional<Error> getError();
/// guess log level from a line of game log
static MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level);
protected:
std::optional<LogEntry> parseAttributes();
void setError();
void clearError();
std::optional<ParsedItem> parseLog4J();
private:
QString m_buffer;
QString m_partialData;
QXmlStreamReader m_parser;
std::optional<Error> m_error;
};

View file

@ -23,7 +23,7 @@
namespace Meta { namespace Meta {
Index::Index(QObject* parent) : QAbstractListModel(parent) {} Index::Index(QObject* parent) : QAbstractListModel(parent) {}
Index::Index(const QVector<VersionList::Ptr>& lists, QObject* parent) : QAbstractListModel(parent), m_lists(lists) Index::Index(const QList<VersionList::Ptr>& lists, QObject* parent) : QAbstractListModel(parent), m_lists(lists)
{ {
for (int i = 0; i < m_lists.size(); ++i) { for (int i = 0; i < m_lists.size(); ++i) {
m_uids.insert(m_lists.at(i)->uid(), m_lists.at(i)); m_uids.insert(m_lists.at(i)->uid(), m_lists.at(i));
@ -103,7 +103,7 @@ void Index::parse(const QJsonObject& obj)
void Index::merge(const std::shared_ptr<Index>& other) void Index::merge(const std::shared_ptr<Index>& other)
{ {
const QVector<VersionList::Ptr> lists = other->m_lists; const QList<VersionList::Ptr> lists = other->m_lists;
// initial load, no need to merge // initial load, no need to merge
if (m_lists.isEmpty()) { if (m_lists.isEmpty()) {
beginResetModel(); beginResetModel();

View file

@ -29,7 +29,7 @@ class Index : public QAbstractListModel, public BaseEntity {
Q_OBJECT Q_OBJECT
public: public:
explicit Index(QObject* parent = nullptr); explicit Index(QObject* parent = nullptr);
explicit Index(const QVector<VersionList::Ptr>& lists, QObject* parent = nullptr); explicit Index(const QList<VersionList::Ptr>& lists, QObject* parent = nullptr);
virtual ~Index() = default; virtual ~Index() = default;
enum { UidRole = Qt::UserRole, NameRole, ListPtrRole }; enum { UidRole = Qt::UserRole, NameRole, ListPtrRole };
@ -46,7 +46,7 @@ class Index : public QAbstractListModel, public BaseEntity {
Version::Ptr get(const QString& uid, const QString& version); Version::Ptr get(const QString& uid, const QString& version);
bool hasUid(const QString& uid) const; bool hasUid(const QString& uid) const;
QVector<VersionList::Ptr> lists() const { return m_lists; } QList<VersionList::Ptr> lists() const { return m_lists; }
Task::Ptr loadVersion(const QString& uid, const QString& version = {}, Net::Mode mode = Net::Mode::Online, bool force = false); Task::Ptr loadVersion(const QString& uid, const QString& version = {}, Net::Mode mode = Net::Mode::Online, bool force = false);
@ -60,7 +60,7 @@ class Index : public QAbstractListModel, public BaseEntity {
void parse(const QJsonObject& obj) override; void parse(const QJsonObject& obj) override;
private: private:
QVector<VersionList::Ptr> m_lists; QList<VersionList::Ptr> m_lists;
QHash<QString, VersionList::Ptr> m_uids; QHash<QString, VersionList::Ptr> m_uids;
void connectVersionList(int row, const VersionList::Ptr& list); void connectVersionList(int row, const VersionList::Ptr& list);

View file

@ -35,8 +35,8 @@ MetadataVersion currentFormatVersion()
// Index // Index
static std::shared_ptr<Index> parseIndexInternal(const QJsonObject& obj) static std::shared_ptr<Index> parseIndexInternal(const QJsonObject& obj)
{ {
const QVector<QJsonObject> objects = requireIsArrayOf<QJsonObject>(obj, "packages"); const QList<QJsonObject> objects = requireIsArrayOf<QJsonObject>(obj, "packages");
QVector<VersionList::Ptr> lists; QList<VersionList::Ptr> lists;
lists.reserve(objects.size()); lists.reserve(objects.size());
std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) { std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) {
VersionList::Ptr list = std::make_shared<VersionList>(requireString(obj, "uid")); VersionList::Ptr list = std::make_shared<VersionList>(requireString(obj, "uid"));
@ -79,8 +79,8 @@ static VersionList::Ptr parseVersionListInternal(const QJsonObject& obj)
{ {
const QString uid = requireString(obj, "uid"); const QString uid = requireString(obj, "uid");
const QVector<QJsonObject> versionsRaw = requireIsArrayOf<QJsonObject>(obj, "versions"); const QList<QJsonObject> versionsRaw = requireIsArrayOf<QJsonObject>(obj, "versions");
QVector<Version::Ptr> versions; QList<Version::Ptr> versions;
versions.reserve(versionsRaw.size()); versions.reserve(versionsRaw.size());
std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [uid](const QJsonObject& vObj) { std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [uid](const QJsonObject& vObj) {
auto version = parseCommonVersion(uid, vObj); auto version = parseCommonVersion(uid, vObj);

View file

@ -19,8 +19,8 @@
#include "BaseVersion.h" #include "BaseVersion.h"
#include <QJsonObject> #include <QJsonObject>
#include <QList>
#include <QStringList> #include <QStringList>
#include <QVector>
#include <memory> #include <memory>
#include "minecraft/VersionFile.h" #include "minecraft/VersionFile.h"

View file

@ -169,7 +169,7 @@ void VersionList::setName(const QString& name)
emit nameChanged(name); emit nameChanged(name);
} }
void VersionList::setVersions(const QVector<Version::Ptr>& versions) void VersionList::setVersions(const QList<Version::Ptr>& versions)
{ {
beginResetModel(); beginResetModel();
m_versions = versions; m_versions = versions;
@ -265,7 +265,7 @@ void VersionList::setupAddedVersion(const int row, const Version::Ptr& version)
disconnect(version.get(), &Version::typeChanged, this, nullptr); disconnect(version.get(), &Version::typeChanged, this, nullptr);
connect(version.get(), &Version::requiresChanged, this, connect(version.get(), &Version::requiresChanged, this,
[this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << RequiresRole); }); [this, row]() { emit dataChanged(index(row), index(row), QList<int>() << RequiresRole); });
connect(version.get(), &Version::timeChanged, this, connect(version.get(), &Version::timeChanged, this,
[this, row]() { emit dataChanged(index(row), index(row), { TimeRole, SortRole }); }); [this, row]() { emit dataChanged(index(row), index(row), { TimeRole, SortRole }); });
connect(version.get(), &Version::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TypeRole }); }); connect(version.get(), &Version::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TypeRole }); });

View file

@ -61,14 +61,14 @@ class VersionList : public BaseVersionList, public BaseEntity {
Version::Ptr getVersion(const QString& version); Version::Ptr getVersion(const QString& version);
bool hasVersion(QString version) const; bool hasVersion(QString version) const;
QVector<Version::Ptr> versions() const { return m_versions; } QList<Version::Ptr> versions() const { return m_versions; }
// this blocks until the version list is loaded // this blocks until the version list is loaded
void waitToLoad(); void waitToLoad();
public: // for usage only by parsers public: // for usage only by parsers
void setName(const QString& name); void setName(const QString& name);
void setVersions(const QVector<Version::Ptr>& versions); void setVersions(const QList<Version::Ptr>& versions);
void merge(const VersionList::Ptr& other); void merge(const VersionList::Ptr& other);
void mergeFromIndex(const VersionList::Ptr& other); void mergeFromIndex(const VersionList::Ptr& other);
void parse(const QJsonObject& obj) override; void parse(const QJsonObject& obj) override;
@ -82,7 +82,7 @@ class VersionList : public BaseVersionList, public BaseEntity {
void updateListData(QList<BaseVersion::Ptr>) override {} void updateListData(QList<BaseVersion::Ptr>) override {}
private: private:
QVector<Version::Ptr> m_versions; QList<Version::Ptr> m_versions;
QStringList m_externalRecommendsVersions; QStringList m_externalRecommendsVersions;
QHash<QString, Version::Ptr> m_lookup; QHash<QString, Version::Ptr> m_lookup;
QString m_uid; QString m_uid;

View file

@ -54,11 +54,11 @@ struct GradleSpecifier {
4 "jdk15" 4 "jdk15"
5 "jar" 5 "jar"
*/ */
QRegularExpression matcher( static const QRegularExpression s_matcher(
QRegularExpression::anchoredPattern("([^:@]+):([^:@]+):([^:@]+)" QRegularExpression::anchoredPattern("([^:@]+):([^:@]+):([^:@]+)"
"(?::([^:@]+))?" "(?::([^:@]+))?"
"(?:@([^:@]+))?")); "(?:@([^:@]+))?"));
QRegularExpressionMatch match = matcher.match(value); QRegularExpressionMatch match = s_matcher.match(value);
m_valid = match.hasMatch(); m_valid = match.hasMatch();
if (!m_valid) { if (!m_valid) {
m_invalidValue = value; m_invalidValue = value;

View file

@ -53,7 +53,6 @@
#include "MMCTime.h" #include "MMCTime.h"
#include "java/JavaVersion.h" #include "java/JavaVersion.h"
#include "pathmatcher/MultiMatcher.h" #include "pathmatcher/MultiMatcher.h"
#include "pathmatcher/RegexpMatcher.h"
#include "launch/LaunchTask.h" #include "launch/LaunchTask.h"
#include "launch/TaskStepWrapper.h" #include "launch/TaskStepWrapper.h"
@ -689,9 +688,9 @@ static QString replaceTokensIn(QString text, QMap<QString, QString> with)
{ {
// TODO: does this still work?? // TODO: does this still work??
QString result; QString result;
QRegularExpression token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption); static const QRegularExpression s_token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption);
QStringList list; QStringList list;
QRegularExpressionMatchIterator i = token_regexp.globalMatch(text); QRegularExpressionMatchIterator i = s_token_regexp.globalMatch(text);
int lastCapturedEnd = 0; int lastCapturedEnd = 0;
while (i.hasNext()) { while (i.hasNext()) {
QRegularExpressionMatch match = i.next(); QRegularExpressionMatch match = i.next();
@ -757,11 +756,7 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine
token_mapping["assets_root"] = absAssetsDir; token_mapping["assets_root"] = absAssetsDir;
token_mapping["assets_index_name"] = assets->id; token_mapping["assets_index_name"] = assets->id;
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QStringList parts = args_pattern.split(' ', Qt::SkipEmptyParts); QStringList parts = args_pattern.split(' ', Qt::SkipEmptyParts);
#else
QStringList parts = args_pattern.split(' ', QString::SkipEmptyParts);
#endif
for (int i = 0; i < parts.length(); i++) { for (int i = 0; i < parts.length(); i++) {
parts[i] = replaceTokensIn(parts[i], token_mapping); parts[i] = replaceTokensIn(parts[i], token_mapping);
} }
@ -816,11 +811,7 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftT
auto mainWindow = qobject_cast<QMainWindow*>(w); auto mainWindow = qobject_cast<QMainWindow*>(w);
if (mainWindow) { if (mainWindow) {
auto m = mainWindow->windowHandle()->frameMargins(); auto m = mainWindow->windowHandle()->frameMargins();
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
screenGeometry = screenGeometry.shrunkBy(m); screenGeometry = screenGeometry.shrunkBy(m);
#else
screenGeometry = { screenGeometry.width() - m.left() - m.right(), screenGeometry.height() - m.top() - m.bottom() };
#endif
break; break;
} }
} }
@ -1012,61 +1003,9 @@ QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSess
return filter; return filter;
} }
MessageLevel::Enum MinecraftInstance::guessLevel(const QString& line, MessageLevel::Enum level) QStringList MinecraftInstance::getLogFileSearchPaths()
{ {
QRegularExpression re("\\[(?<timestamp>[0-9:]+)\\] \\[[^/]+/(?<level>[^\\]]+)\\]"); return { FS::PathCombine(gameRoot(), "crash-reports"), FS::PathCombine(gameRoot(), "logs"), gameRoot() };
auto match = re.match(line);
if (match.hasMatch()) {
// New style logs from log4j
QString timestamp = match.captured("timestamp");
QString levelStr = match.captured("level");
if (levelStr == "INFO")
level = MessageLevel::Message;
if (levelStr == "WARN")
level = MessageLevel::Warning;
if (levelStr == "ERROR")
level = MessageLevel::Error;
if (levelStr == "FATAL")
level = MessageLevel::Fatal;
if (levelStr == "TRACE" || levelStr == "DEBUG")
level = MessageLevel::Debug;
} else {
// Old style forge logs
if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") ||
line.contains("[FINEST]"))
level = MessageLevel::Message;
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 (line.contains("overwriting existing"))
return MessageLevel::Fatal;
// NOTE: this diverges from the real regexp. no unicode, the first section is + instead of *
static const QString javaSymbol = "([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$][a-zA-Z\\d_$]*";
if (line.contains("Exception in thread") || line.contains(QRegularExpression("\\s+at " + javaSymbol)) ||
line.contains(QRegularExpression("Caused by: " + javaSymbol)) ||
line.contains(QRegularExpression("([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$]?[a-zA-Z\\d_$]*(Exception|Error|Throwable)")) ||
line.contains(QRegularExpression("... \\d+ more$")))
return MessageLevel::Error;
return level;
}
IPathMatcher::Ptr MinecraftInstance::getLogFileMatcher()
{
auto combined = std::make_shared<MultiMatcher>();
combined->add(std::make_shared<RegexpMatcher>(".*\\.log(\\.[0-9]*)?(\\.gz)?$"));
combined->add(std::make_shared<RegexpMatcher>("crash-.*\\.txt"));
combined->add(std::make_shared<RegexpMatcher>("IDMap dump.*\\.txt$"));
combined->add(std::make_shared<RegexpMatcher>("ModLoader\\.txt(\\..*)?$"));
return combined;
}
QString MinecraftInstance::getLogFileRoot()
{
return gameRoot();
} }
QString MinecraftInstance::getStatusbarDescription() QString MinecraftInstance::getStatusbarDescription()

View file

@ -139,12 +139,7 @@ class MinecraftInstance : public BaseInstance {
QProcessEnvironment createEnvironment() override; QProcessEnvironment createEnvironment() override;
QProcessEnvironment createLaunchEnvironment() override; QProcessEnvironment createLaunchEnvironment() override;
/// guess log level from a line of minecraft log QStringList getLogFileSearchPaths() override;
MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level) override;
IPathMatcher::Ptr getLogFileMatcher() override;
QString getLogFileRoot() override;
QString getStatusbarDescription() override; QString getStatusbarDescription() override;

View file

@ -114,9 +114,9 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc
out->uid = root.value("fileId").toString(); out->uid = root.value("fileId").toString();
} }
const QRegularExpression valid_uid_regex{ QRegularExpression::anchoredPattern( static const QRegularExpression s_validUidRegex{ QRegularExpression::anchoredPattern(
QStringLiteral(R"([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*)")) }; QStringLiteral(R"([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*)")) };
if (!valid_uid_regex.match(out->uid).hasMatch()) { if (!s_validUidRegex.match(out->uid).hasMatch()) {
qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid; qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid;
out->addProblem(ProblemSeverity::Error, out->addProblem(ProblemSeverity::Error,
QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues.")); QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues."));

View file

@ -645,11 +645,7 @@ void PackProfile::move(const int index, const MoveDirection direction)
return; return;
} }
beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap);
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
d->components.swapItemsAt(index, theirIndex); d->components.swapItemsAt(index, theirIndex);
#else
d->components.swap(index, theirIndex);
#endif
endMoveRows(); endMoveRows();
invalidateLaunchProfile(); invalidateLaunchProfile();
scheduleSave(); scheduleSave();

View file

@ -41,7 +41,6 @@
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QRegularExpression>
#include <QSaveFile> #include <QSaveFile>
namespace ProfileUtils { namespace ProfileUtils {

View file

@ -0,0 +1,237 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (C) 2025 Yihe Li <winmikedows@hotmail.com>
*
* parent program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* parent program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with parent program. If not, see <https://www.gnu.org/licenses/>.
*
* parent file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use parent file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ShortcutUtils.h"
#include "FileSystem.h"
#include <QApplication>
#include <QFileDialog>
#include <BuildConfig.h>
#include <DesktopServices.h>
#include <icons/IconList.h>
namespace ShortcutUtils {
void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath)
{
if (!shortcut.instance)
return;
QString appPath = QApplication::applicationFilePath();
auto icon = APPLICATION->icons()->icon(shortcut.iconKey.isEmpty() ? shortcut.instance->iconKey() : shortcut.iconKey);
if (icon == nullptr) {
icon = APPLICATION->icons()->icon("grass");
}
QString iconPath;
QStringList args;
#if defined(Q_OS_MACOS)
if (appPath.startsWith("/private/var/")) {
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"),
QObject::tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts."));
return;
}
iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "Icon.icns");
QFile iconFile(iconPath);
if (!iconFile.open(QFile::WriteOnly)) {
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application."));
return;
}
QIcon iconObj = icon->icon();
bool success = iconObj.pixmap(1024, 1024).save(iconPath, "ICNS");
iconFile.close();
if (!success) {
iconFile.remove();
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application."));
return;
}
#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
if (appPath.startsWith("/tmp/.mount_")) {
// AppImage!
appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE"));
if (appPath.isEmpty()) {
QMessageBox::critical(
shortcut.parent, QObject::tr("Create Shortcut"),
QObject::tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)"));
} else if (appPath.endsWith("/")) {
appPath.chop(1);
}
}
iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.png");
QFile iconFile(iconPath);
if (!iconFile.open(QFile::WriteOnly)) {
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut."));
return;
}
bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG");
iconFile.close();
if (!success) {
iconFile.remove();
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut."));
return;
}
if (DesktopServices::isFlatpak()) {
appPath = "flatpak";
args.append({ "run", BuildConfig.LAUNCHER_APPID });
}
#elif defined(Q_OS_WIN)
iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.ico");
// part of fix for weird bug involving the window icon being replaced
// dunno why it happens, but parent 2-line fix seems to be enough, so w/e
auto appIcon = APPLICATION->getThemedIcon("logo");
QFile iconFile(iconPath);
if (!iconFile.open(QFile::WriteOnly)) {
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut."));
return;
}
bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO");
iconFile.close();
// restore original window icon
QGuiApplication::setWindowIcon(appIcon);
if (!success) {
iconFile.remove();
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut."));
return;
}
#else
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Not supported on your platform!"));
return;
#endif
args.append({ "--launch", shortcut.instance->id() });
args.append(shortcut.extraArgs);
if (!FS::createShortcut(filePath, appPath, args, shortcut.name, iconPath)) {
#if not defined(Q_OS_MACOS)
iconFile.remove();
#endif
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"),
QObject::tr("Failed to create %1 shortcut!").arg(shortcut.targetString));
}
}
void createInstanceShortcutOnDesktop(const Shortcut& shortcut)
{
if (!shortcut.instance)
return;
QString desktopDir = FS::getDesktopDir();
if (desktopDir.isEmpty()) {
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find desktop?!"));
return;
}
QString shortcutFilePath = FS::PathCombine(desktopDir, FS::RemoveInvalidFilenameChars(shortcut.name));
createInstanceShortcut(shortcut, shortcutFilePath);
QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"),
QObject::tr("Created a shortcut to this %1 on your desktop!").arg(shortcut.targetString));
}
void createInstanceShortcutInApplications(const Shortcut& shortcut)
{
if (!shortcut.instance)
return;
QString applicationsDir = FS::getApplicationsDir();
if (applicationsDir.isEmpty()) {
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find applications folder?!"));
return;
}
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
applicationsDir = FS::PathCombine(applicationsDir, BuildConfig.LAUNCHER_DISPLAYNAME + " Instances");
QDir applicationsDirQ(applicationsDir);
if (!applicationsDirQ.mkpath(".")) {
QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"),
QObject::tr("Failed to create instances folder in applications folder!"));
return;
}
#endif
QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(shortcut.name));
createInstanceShortcut(shortcut, shortcutFilePath);
QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"),
QObject::tr("Created a shortcut to this %1 in your applications folder!").arg(shortcut.targetString));
}
void createInstanceShortcutInOther(const Shortcut& shortcut)
{
if (!shortcut.instance)
return;
QString defaultedDir = FS::getDesktopDir();
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
QString extension = ".desktop";
#elif defined(Q_OS_WINDOWS)
QString extension = ".lnk";
#else
QString extension = "";
#endif
QString shortcutFilePath = FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(shortcut.name) + extension);
QFileDialog fileDialog;
// workaround to make sure the portal file dialog opens in the desktop directory
fileDialog.setDirectoryUrl(defaultedDir);
shortcutFilePath = fileDialog.getSaveFileName(shortcut.parent, QObject::tr("Create Shortcut"), shortcutFilePath,
QObject::tr("Desktop Entries") + " (*" + extension + ")");
if (shortcutFilePath.isEmpty())
return; // file dialog canceled by user
if (shortcutFilePath.endsWith(extension))
shortcutFilePath = shortcutFilePath.mid(0, shortcutFilePath.length() - extension.length());
createInstanceShortcut(shortcut, shortcutFilePath);
QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"),
QObject::tr("Created a shortcut to this %1!").arg(shortcut.targetString));
}
} // namespace ShortcutUtils

View file

@ -0,0 +1,66 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (C) 2025 Yihe Li <winmikedows@hotmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "Application.h"
#include <QMessageBox>
namespace ShortcutUtils {
/// A struct to hold parameters for creating a shortcut
struct Shortcut {
BaseInstance* instance;
QString name;
QString targetString;
QWidget* parent = nullptr;
QStringList extraArgs = {};
QString iconKey = "";
};
/// Create an instance shortcut on the specified file path
void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath);
/// Create an instance shortcut on the desktop
void createInstanceShortcutOnDesktop(const Shortcut& shortcut);
/// Create an instance shortcut in the Applications directory
void createInstanceShortcutInApplications(const Shortcut& shortcut);
/// Create an instance shortcut in other directories
void createInstanceShortcutInOther(const Shortcut& shortcut);
} // namespace ShortcutUtils

View file

@ -198,22 +198,6 @@ bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data)
return f.commit(); return f.commit();
} }
int64_t calculateWorldSize(const QFileInfo& file)
{
if (file.isFile() && file.suffix() == "zip") {
return file.size();
} else if (file.isDir()) {
QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories);
int64_t total = 0;
while (it.hasNext()) {
it.next();
total += it.fileInfo().size();
}
return total;
}
return -1;
}
World::World(const QFileInfo& file) World::World(const QFileInfo& file)
{ {
repath(file); repath(file);
@ -223,7 +207,6 @@ void World::repath(const QFileInfo& file)
{ {
m_containerFile = file; m_containerFile = file;
m_folderName = file.fileName(); m_folderName = file.fileName();
m_size = calculateWorldSize(file);
if (file.isFile() && file.suffix() == "zip") { if (file.isFile() && file.suffix() == "zip") {
m_iconFile = QString(); m_iconFile = QString();
readFromZip(file); readFromZip(file);
@ -252,41 +235,41 @@ void World::readFromFS(const QFileInfo& file)
{ {
auto bytes = getLevelDatDataFromFS(file); auto bytes = getLevelDatDataFromFS(file);
if (bytes.isEmpty()) { if (bytes.isEmpty()) {
is_valid = false; m_isValid = false;
return; return;
} }
loadFromLevelDat(bytes); loadFromLevelDat(bytes);
levelDatTime = file.lastModified(); m_levelDatTime = file.lastModified();
} }
void World::readFromZip(const QFileInfo& file) void World::readFromZip(const QFileInfo& file)
{ {
QuaZip zip(file.absoluteFilePath()); QuaZip zip(file.absoluteFilePath());
is_valid = zip.open(QuaZip::mdUnzip); m_isValid = zip.open(QuaZip::mdUnzip);
if (!is_valid) { if (!m_isValid) {
return; return;
} }
auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat"); auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat");
is_valid = !location.isEmpty(); m_isValid = !location.isEmpty();
if (!is_valid) { if (!m_isValid) {
return; return;
} }
m_containerOffsetPath = location; m_containerOffsetPath = location;
QuaZipFile zippedFile(&zip); QuaZipFile zippedFile(&zip);
// read the install profile // read the install profile
is_valid = zip.setCurrentFile(location + "level.dat"); m_isValid = zip.setCurrentFile(location + "level.dat");
if (!is_valid) { if (!m_isValid) {
return; return;
} }
is_valid = zippedFile.open(QIODevice::ReadOnly); m_isValid = zippedFile.open(QIODevice::ReadOnly);
QuaZipFileInfo64 levelDatInfo; QuaZipFileInfo64 levelDatInfo;
zippedFile.getFileInfo(&levelDatInfo); zippedFile.getFileInfo(&levelDatInfo);
auto modTime = levelDatInfo.getNTFSmTime(); auto modTime = levelDatInfo.getNTFSmTime();
if (!modTime.isValid()) { if (!modTime.isValid()) {
modTime = levelDatInfo.dateTime; modTime = levelDatInfo.dateTime;
} }
levelDatTime = modTime; m_levelDatTime = modTime;
if (!is_valid) { if (!m_isValid) {
return; return;
} }
loadFromLevelDat(zippedFile.readAll()); loadFromLevelDat(zippedFile.readAll());
@ -430,7 +413,7 @@ void World::loadFromLevelDat(QByteArray data)
{ {
auto levelData = parseLevelDat(data); auto levelData = parseLevelDat(data);
if (!levelData) { if (!levelData) {
is_valid = false; m_isValid = false;
return; return;
} }
@ -439,20 +422,20 @@ void World::loadFromLevelDat(QByteArray data)
valPtr = &levelData->at("Data"); valPtr = &levelData->at("Data");
} catch (const std::out_of_range& e) { } catch (const std::out_of_range& e) {
qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what(); qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what();
is_valid = false; m_isValid = false;
return; return;
} }
nbt::value& val = *valPtr; nbt::value& val = *valPtr;
is_valid = val.get_type() == nbt::tag_type::Compound; m_isValid = val.get_type() == nbt::tag_type::Compound;
if (!is_valid) if (!m_isValid)
return; return;
auto name = read_string(val, "LevelName"); auto name = read_string(val, "LevelName");
m_actualName = name ? *name : m_folderName; m_actualName = name ? *name : m_folderName;
auto timestamp = read_long(val, "LastPlayed"); auto timestamp = read_long(val, "LastPlayed");
m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : levelDatTime; m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : m_levelDatTime;
m_gameType = read_gametype(val, "GameType"); m_gameType = read_gametype(val, "GameType");
@ -490,7 +473,7 @@ bool World::replace(World& with)
bool World::destroy() bool World::destroy()
{ {
if (!is_valid) if (!m_isValid)
return false; return false;
if (FS::trash(m_containerFile.filePath())) if (FS::trash(m_containerFile.filePath()))
@ -508,7 +491,7 @@ bool World::destroy()
bool World::operator==(const World& other) const bool World::operator==(const World& other) const
{ {
return is_valid == other.is_valid && folderName() == other.folderName(); return m_isValid == other.m_isValid && folderName() == other.folderName();
} }
bool World::isSymLinkUnder(const QString& instPath) const bool World::isSymLinkUnder(const QString& instPath) const
@ -531,3 +514,8 @@ bool World::isMoreThanOneHardLink() const
} }
return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1; return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1;
} }
void World::setSize(int64_t size)
{
m_size = size;
}

View file

@ -39,7 +39,7 @@ class World {
QDateTime lastPlayed() const { return m_lastPlayed; } QDateTime lastPlayed() const { return m_lastPlayed; }
GameType gameType() const { return m_gameType; } GameType gameType() const { return m_gameType; }
int64_t seed() const { return m_randomSeed; } int64_t seed() const { return m_randomSeed; }
bool isValid() const { return is_valid; } bool isValid() const { return m_isValid; }
bool isOnFS() const { return m_containerFile.isDir(); } bool isOnFS() const { return m_containerFile.isDir(); }
QFileInfo container() const { return m_containerFile; } QFileInfo container() const { return m_containerFile; }
// delete all the files of this world // delete all the files of this world
@ -54,6 +54,8 @@ class World {
bool rename(const QString& to); bool rename(const QString& to);
bool install(const QString& to, const QString& name = QString()); bool install(const QString& to, const QString& name = QString());
void setSize(int64_t size);
// WEAK compare operator - used for replacing worlds // WEAK compare operator - used for replacing worlds
bool operator==(const World& other) const; bool operator==(const World& other) const;
@ -83,10 +85,10 @@ class World {
QString m_folderName; QString m_folderName;
QString m_actualName; QString m_actualName;
QString m_iconFile; QString m_iconFile;
QDateTime levelDatTime; QDateTime m_levelDatTime;
QDateTime m_lastPlayed; QDateTime m_lastPlayed;
int64_t m_size; int64_t m_size;
int64_t m_randomSeed = 0; int64_t m_randomSeed = 0;
GameType m_gameType; GameType m_gameType;
bool is_valid = false; bool m_isValid = false;
}; };

View file

@ -37,13 +37,14 @@
#include <FileSystem.h> #include <FileSystem.h>
#include <QDebug> #include <QDebug>
#include <QDirIterator>
#include <QFileSystemWatcher> #include <QFileSystemWatcher>
#include <QMimeData> #include <QMimeData>
#include <QString> #include <QString>
#include <QThreadPool>
#include <QUrl> #include <QUrl>
#include <QUuid> #include <QUuid>
#include <Qt> #include <Qt>
#include "Application.h"
WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractListModel(), m_instance(instance), m_dir(dir) WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractListModel(), m_instance(instance), m_dir(dir)
{ {
@ -51,18 +52,18 @@ WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractList
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
m_watcher = new QFileSystemWatcher(this); m_watcher = new QFileSystemWatcher(this);
is_watching = false; m_isWatching = false;
connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &WorldList::directoryChanged); connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &WorldList::directoryChanged);
} }
void WorldList::startWatching() void WorldList::startWatching()
{ {
if (is_watching) { if (m_isWatching) {
return; return;
} }
update(); update();
is_watching = m_watcher->addPath(m_dir.absolutePath()); m_isWatching = m_watcher->addPath(m_dir.absolutePath());
if (is_watching) { if (m_isWatching) {
qDebug() << "Started watching " << m_dir.absolutePath(); qDebug() << "Started watching " << m_dir.absolutePath();
} else { } else {
qDebug() << "Failed to start watching " << m_dir.absolutePath(); qDebug() << "Failed to start watching " << m_dir.absolutePath();
@ -71,11 +72,11 @@ void WorldList::startWatching()
void WorldList::stopWatching() void WorldList::stopWatching()
{ {
if (!is_watching) { if (!m_isWatching) {
return; return;
} }
is_watching = !m_watcher->removePath(m_dir.absolutePath()); m_isWatching = !m_watcher->removePath(m_dir.absolutePath());
if (!is_watching) { if (!m_isWatching) {
qDebug() << "Stopped watching " << m_dir.absolutePath(); qDebug() << "Stopped watching " << m_dir.absolutePath();
} else { } else {
qDebug() << "Failed to stop watching " << m_dir.absolutePath(); qDebug() << "Failed to stop watching " << m_dir.absolutePath();
@ -101,12 +102,13 @@ bool WorldList::update()
} }
} }
beginResetModel(); beginResetModel();
worlds.swap(newWorlds); m_worlds.swap(newWorlds);
endResetModel(); endResetModel();
loadWorldsAsync();
return true; return true;
} }
void WorldList::directoryChanged(QString path) void WorldList::directoryChanged(QString)
{ {
update(); update();
} }
@ -123,12 +125,12 @@ QString WorldList::instDirPath() const
bool WorldList::deleteWorld(int index) bool WorldList::deleteWorld(int index)
{ {
if (index >= worlds.size() || index < 0) if (index >= m_worlds.size() || index < 0)
return false; return false;
World& m = worlds[index]; World& m = m_worlds[index];
if (m.destroy()) { if (m.destroy()) {
beginRemoveRows(QModelIndex(), index, index); beginRemoveRows(QModelIndex(), index, index);
worlds.removeAt(index); m_worlds.removeAt(index);
endRemoveRows(); endRemoveRows();
emit changed(); emit changed();
return true; return true;
@ -139,11 +141,11 @@ bool WorldList::deleteWorld(int index)
bool WorldList::deleteWorlds(int first, int last) bool WorldList::deleteWorlds(int first, int last)
{ {
for (int i = first; i <= last; i++) { for (int i = first; i <= last; i++) {
World& m = worlds[i]; World& m = m_worlds[i];
m.destroy(); m.destroy();
} }
beginRemoveRows(QModelIndex(), first, last); beginRemoveRows(QModelIndex(), first, last);
worlds.erase(worlds.begin() + first, worlds.begin() + last + 1); m_worlds.erase(m_worlds.begin() + first, m_worlds.begin() + last + 1);
endRemoveRows(); endRemoveRows();
emit changed(); emit changed();
return true; return true;
@ -151,9 +153,9 @@ bool WorldList::deleteWorlds(int first, int last)
bool WorldList::resetIcon(int row) bool WorldList::resetIcon(int row)
{ {
if (row >= worlds.size() || row < 0) if (row >= m_worlds.size() || row < 0)
return false; return false;
World& m = worlds[row]; World& m = m_worlds[row];
if (m.resetIcon()) { if (m.resetIcon()) {
emit dataChanged(index(row), index(row), { WorldList::IconFileRole }); emit dataChanged(index(row), index(row), { WorldList::IconFileRole });
return true; return true;
@ -174,12 +176,12 @@ QVariant WorldList::data(const QModelIndex& index, int role) const
int row = index.row(); int row = index.row();
int column = index.column(); int column = index.column();
if (row < 0 || row >= worlds.size()) if (row < 0 || row >= m_worlds.size())
return QVariant(); return QVariant();
QLocale locale; QLocale locale;
auto& world = worlds[row]; auto& world = m_worlds[row];
switch (role) { switch (role) {
case Qt::DisplayRole: case Qt::DisplayRole:
switch (column) { switch (column) {
@ -307,11 +309,7 @@ class WorldMimeData : public QMimeData {
QStringList formats() const { return QMimeData::formats() << "text/uri-list"; } QStringList formats() const { return QMimeData::formats() << "text/uri-list"; }
protected: protected:
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QVariant retrieveData(const QString& mimetype, QMetaType type) const QVariant retrieveData(const QString& mimetype, QMetaType type) const
#else
QVariant retrieveData(const QString& mimetype, QVariant::Type type) const
#endif
{ {
QList<QUrl> urls; QList<QUrl> urls;
for (auto& world : m_worlds) { for (auto& world : m_worlds) {
@ -339,9 +337,9 @@ QMimeData* WorldList::mimeData(const QModelIndexList& indexes) const
if (idx.column() != 0) if (idx.column() != 0)
continue; continue;
int row = idx.row(); int row = idx.row();
if (row < 0 || row >= this->worlds.size()) if (row < 0 || row >= this->m_worlds.size())
continue; continue;
worlds_.append(this->worlds[row]); worlds_.append(this->m_worlds[row]);
} }
if (!worlds_.size()) { if (!worlds_.size()) {
return new QMimeData(); return new QMimeData();
@ -393,7 +391,7 @@ bool WorldList::dropMimeData(const QMimeData* data,
return false; return false;
// files dropped from outside? // files dropped from outside?
if (data->hasUrls()) { if (data->hasUrls()) {
bool was_watching = is_watching; bool was_watching = m_isWatching;
if (was_watching) if (was_watching)
stopWatching(); stopWatching();
auto urls = data->urls(); auto urls = data->urls();
@ -416,4 +414,44 @@ bool WorldList::dropMimeData(const QMimeData* data,
return false; return false;
} }
int64_t calculateWorldSize(const QFileInfo& file)
{
if (file.isFile() && file.suffix() == "zip") {
return file.size();
} else if (file.isDir()) {
QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories);
int64_t total = 0;
while (it.hasNext()) {
it.next();
total += it.fileInfo().size();
}
return total;
}
return -1;
}
void WorldList::loadWorldsAsync()
{
for (int i = 0; i < m_worlds.size(); ++i) {
auto file = m_worlds.at(i).container();
int row = i;
QThreadPool::globalInstance()->start([this, file, row]() mutable {
auto size = calculateWorldSize(file);
QMetaObject::invokeMethod(
this,
[this, size, row, file]() {
if (row < m_worlds.size() && m_worlds[row].container() == file) {
m_worlds[row].setSize(size);
// Notify views
QModelIndex modelIndex = index(row);
emit dataChanged(modelIndex, modelIndex, { SizeRole });
}
},
Qt::QueuedConnection);
});
}
}
#include "WorldList.moc" #include "WorldList.moc"

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