diff --git a/.envrc b/.envrc
index 190b5b2b3..1d11c5354 100644
--- a/.envrc
+++ b/.envrc
@@ -1,2 +1,2 @@
-use flake
+use nix
watch_file nix/*.nix
diff --git a/.github/actions/get-merge-commit/action.yml b/.github/actions/get-merge-commit/action.yml
new file mode 100644
index 000000000..534d138e1
--- /dev/null
+++ b/.github/actions/get-merge-commit/action.yml
@@ -0,0 +1,103 @@
+# This file incorporates work covered by the following copyright and
+# permission notice
+#
+# Copyright (c) 2003-2025 Eelco Dolstra and the Nixpkgs/NixOS contributors
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+name: Get merge commit
+description: Get a merge commit of a given pull request
+
+inputs:
+ repository:
+ description: Repository containing the pull request
+ required: false
+ pull-request-id:
+ description: ID of a pull request
+ required: true
+
+outputs:
+ merge-commit-sha:
+ description: Git SHA of a merge commit
+ value: ${{ steps.query.outputs.merge-commit-sha }}
+
+runs:
+ using: composite
+
+ steps:
+ - name: Wait for GitHub to report merge commit
+ id: query
+ shell: bash
+ env:
+ GITHUB_REPO: ${{ inputs.repository || github.repository }}
+ PR_ID: ${{ inputs.pull-request-id }}
+ # https://github.com/NixOS/nixpkgs/blob/8f77f3600f1ee775b85dc2c72fd842768e486ec9/ci/get-merge-commit.sh
+ run: |
+ set -euo pipefail
+
+ log() {
+ echo "$@" >&2
+ }
+
+ # Retry the API query this many times
+ retryCount=5
+ # Start with 5 seconds, but double every retry
+ retryInterval=5
+
+ while true; do
+ log "Checking whether the pull request can be merged"
+ prInfo=$(gh api \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "/repos/$GITHUB_REPO/pulls/$PR_ID")
+
+ # Non-open PRs won't have their mergeability computed no matter what
+ state=$(jq -r .state <<<"$prInfo")
+ if [[ "$state" != open ]]; then
+ log "PR is not open anymore"
+ exit 1
+ fi
+
+ mergeable=$(jq -r .mergeable <<<"$prInfo")
+ if [[ "$mergeable" == "null" ]]; then
+ if ((retryCount == 0)); then
+ log "Not retrying anymore. It's likely that GitHub is having internal issues: check https://www.githubstatus.com/"
+ exit 3
+ else
+ ((retryCount -= 1)) || true
+
+ # null indicates that GitHub is still computing whether it's mergeable
+ # Wait a couple seconds before trying again
+ log "GitHub is still computing whether this PR can be merged, waiting $retryInterval seconds before trying again ($retryCount retries left)"
+ sleep "$retryInterval"
+
+ ((retryInterval *= 2)) || true
+ fi
+ else
+ break
+ fi
+ done
+
+ if [[ "$mergeable" == "true" ]]; then
+ echo "merge-commit-sha=$(jq -r .merge_commit_sha <<<"$prInfo")" >> "$GITHUB_OUTPUT"
+ else
+ echo "# 🚨 The PR has a merge conflict!" >> "$GITHUB_STEP_SUMMARY"
+ exit 2
+ fi
diff --git a/.github/workflows/blocked-prs.yml b/.github/workflows/blocked-prs.yml
new file mode 100644
index 000000000..bd49b7230
--- /dev/null
+++ b/.github/workflows/blocked-prs.yml
@@ -0,0 +1,239 @@
+name: Blocked/Stacked Pull Requests Automation
+
+on:
+ pull_request_target:
+ types:
+ - opened
+ - reopened
+ - edited
+ - synchronize
+ workflow_dispatch:
+ inputs:
+ pr_id:
+ description: Local Pull Request number to work on
+ required: true
+ type: number
+
+jobs:
+ blocked_status:
+ name: Check Blocked Status
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Generate token
+ id: generate-token
+ uses: actions/create-github-app-token@v2
+ with:
+ app-id: ${{ vars.PULL_REQUEST_APP_ID }}
+ private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }}
+
+ - name: Setup From Dispatch Event
+ if: github.event_name == 'workflow_dispatch'
+ id: dispatch_event_setup
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ PR_NUMBER: ${{ inputs.pr_id }}
+ run: |
+ # setup env for the rest of the workflow
+ OWNER=$(dirname "${{ github.repository }}")
+ REPO=$(basename "${{ github.repository }}")
+ PR_JSON=$(
+ gh api \
+ -H "Accept: application/vnd.github.raw+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "/repos/$OWNER/$REPO/pulls/$PR_NUMBER"
+ )
+ echo "PR_JSON=$PR_JSON" >> "$GITHUB_ENV"
+
+ - name: Setup Environment
+ id: env_setup
+ env:
+ EVENT_PR_JSON: ${{ toJSON(github.event.pull_request) }}
+ run: |
+ # setup env for the rest of the workflow
+ PR_JSON=${PR_JSON:-"$EVENT_PR_JSON"}
+ {
+ echo "REPO=$(jq -r '.base.repo.name' <<< "$PR_JSON")"
+ echo "OWNER=$(jq -r '.base.repo.owner.login' <<< "$PR_JSON")"
+ echo "PR_NUMBER=$(jq -r '.number' <<< "$PR_JSON")"
+ echo "JOB_DATA=$(jq -c '
+ {
+ "repo": .base.repo.name,
+ "owner": .base.repo.owner.login,
+ "repoUrl": .base.repo.html_url,
+ "prNumber": .number,
+ "prHeadSha": .head.sha,
+ "prHeadLabel": .head.label,
+ "prBody": .body,
+ "prLabels": (reduce .labels[].name as $l ([]; . + [$l]))
+ }
+ ' <<< "$PR_JSON")"
+ } >> "$GITHUB_ENV"
+
+
+ - name: Find Blocked/Stacked PRs in body
+ id: pr_ids
+ run: |
+ prs=$(
+ jq -c '
+ .prBody as $body
+ | (
+ $body |
+ reduce (
+ . | scan("blocked (?:by|on):? #([0-9]+)")
+ | map({
+ "type": "Blocked on",
+ "number": ( . | tonumber )
+ })
+ ) as $i ([]; . + [$i[]])
+ ) as $bprs
+ | (
+ $body |
+ reduce (
+ . | scan("stacked on:? #([0-9]+)")
+ | map({
+ "type": "Stacked on",
+ "number": ( . | tonumber )
+ })
+ ) as $i ([]; . + [$i[]])
+ ) as $sprs
+ | ($bprs + $sprs) as $prs
+ | {
+ "blocking": $prs,
+ "numBlocking": ( $prs | length),
+ }
+ ' <<< "$JOB_DATA"
+ )
+ echo "prs=$prs" >> "$GITHUB_OUTPUT"
+
+ - name: Collect Blocked PR Data
+ id: blocking_data
+ if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ BLOCKING_PRS: ${{ steps.pr_ids.outputs.prs }}
+ run: |
+ blocked_pr_data=$(
+ while read -r pr_data ; do
+ gh api \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "/repos/$OWNER/$REPO/pulls/$(jq -r '.number' <<< "$pr_data")" \
+ | jq -c --arg type "$(jq -r '.type' <<< "$pr_data")" \
+ '
+ . | {
+ "type": $type,
+ "number": .number,
+ "merged": .merged,
+ "labels": (reduce .labels[].name as $l ([]; . + [$l])),
+ "basePrUrl": .html_url,
+ "baseRepoName": .head.repo.name,
+ "baseRepoOwner": .head.repo.owner.login,
+ "baseRepoUrl": .head.repo.html_url,
+ "baseSha": .head.sha,
+ "baseRefName": .head.ref,
+ }
+ '
+ done < <(jq -c '.blocking[]' <<< "$BLOCKING_PRS") | jq -c -s
+ )
+ {
+ echo "data=$blocked_pr_data";
+ echo "all_merged=$(jq -r 'all(.[].merged; .)' <<< "$blocked_pr_data")";
+ echo "current_blocking=$(jq -c 'map( select( .merged | not ) | .number )' <<< "$blocked_pr_data" )";
+ } >> "$GITHUB_OUTPUT"
+
+ - name: Add 'blocked' Label is Missing
+ id: label_blocked
+ if: (fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0) && !contains(fromJSON(env.JOB_DATA).prLabels, 'blocked') && !fromJSON(steps.blocking_data.outputs.all_merged)
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ run: |
+ gh -R ${{ github.repository }} issue edit --add-label 'blocked' "$PR_NUMBER"
+
+ - name: Remove 'blocked' Label if All Dependencies Are Merged
+ id: unlabel_blocked
+ if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 && fromJSON(steps.blocking_data.outputs.all_merged)
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ run: |
+ gh -R ${{ github.repository }} issue edit --remove-label 'blocked' "$PR_NUMBER"
+
+ - name: Apply 'blocking' Label to Unmerged Dependencies
+ id: label_blocking
+ if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ BLOCKING_ISSUES: ${{ steps.blocking_data.outputs.current_blocking }}
+ run: |
+ while read -r pr ; do
+ gh -R ${{ github.repository }} issue edit --add-label 'blocking' "$pr" || true
+ done < <(jq -c '.[]' <<< "$BLOCKING_ISSUES")
+
+ - name: Apply Blocking PR Status Check
+ id: blocked_check
+ if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ BLOCKING_DATA: ${{ steps.blocking_data.outputs.data }}
+ run: |
+ pr_head_sha=$(jq -r '.prHeadSha' <<< "$JOB_DATA")
+ # create commit Status, overwrites previous identical context
+ while read -r pr_data ; do
+ DESC=$(
+ jq -r ' "Blocking PR #" + (.number | tostring) + " is " + (if .merged then "" else "not yet " end) + "merged"' <<< "$pr_data"
+ )
+ gh api \
+ --method POST \
+ -H "Accept: application/vnd.github+json" \
+ -H "X-GitHub-Api-Version: 2022-11-28" \
+ "/repos/${OWNER}/${REPO}/statuses/${pr_head_sha}" \
+ -f "state=$(jq -r 'if .merged then "success" else "failure" end' <<< "$pr_data")" \
+ -f "target_url=$(jq -r '.basePrUrl' <<< "$pr_data" )" \
+ -f "description=$DESC" \
+ -f "context=ci/blocking-pr-check:$(jq '.number' <<< "$pr_data")"
+ done < <(jq -c '.[]' <<< "$BLOCKING_DATA")
+
+ - name: Context Comment
+ id: generate-comment
+ if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
+ continue-on-error: true
+ env:
+ BLOCKING_DATA: ${{ steps.blocking_data.outputs.data }}
+ run: |
+ COMMENT_PATH="$(pwd)/temp_comment_file.txt"
+ echo '
PR Dependencies :pushpin:
' > "$COMMENT_PATH"
+ echo >> "$COMMENT_PATH"
+ pr_head_label=$(jq -r '.prHeadLabel' <<< "$JOB_DATA")
+ while read -r pr_data ; do
+ base_pr=$(jq -r '.number' <<< "$pr_data")
+ base_ref_name=$(jq -r '.baseRefName' <<< "$pr_data")
+ base_repo_owner=$(jq -r '.baseRepoOwner' <<< "$pr_data")
+ base_repo_name=$(jq -r '.baseRepoName' <<< "$pr_data")
+ compare_url="https://github.com/$base_repo_owner/$base_repo_name/compare/$base_ref_name...$pr_head_label"
+ status=$(jq -r 'if .merged then ":heavy_check_mark: Merged" else ":x: Not Merged" end' <<< "$pr_data")
+ type=$(jq -r '.type' <<< "$pr_data")
+ echo " - $type #$base_pr $status [(compare)]($compare_url)" >> "$COMMENT_PATH"
+ done < <(jq -c '.[]' <<< "$BLOCKING_DATA")
+
+ {
+ echo 'body<> "$GITHUB_OUTPUT"
+
+ - name: 💬 PR Comment
+ if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ COMMENT_BODY: ${{ steps.generate-comment.outputs.body }}
+ run: |
+ gh -R ${{ github.repository }} issue comment "$PR_NUMBER" \
+ --body "$COMMENT_BODY" \
+ --create-if-none \
+ --edit-last
+
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c5e459914..952b7c515 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -52,19 +52,15 @@ jobs:
fail-fast: false
matrix:
include:
- - os: ubuntu-20.04
- qt_ver: 5
- qt_host: 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.5.3"
+ 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
name: "Windows-MinGW-w64"
@@ -106,14 +102,6 @@ jobs:
qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
- - os: macos-14
- name: macOS-Legacy
- macosx_deployment_target: 10.13
- qt_ver: 5
- qt_host: mac
- qt_version: "5.15.2"
- qt_modules: "qtnetworkauth"
-
runs-on: ${{ matrix.os }}
env:
@@ -164,10 +152,16 @@ jobs:
- name: Setup ccache
if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug'
- uses: hendrikmuhs/ccache-action@v1.2.17
+ uses: hendrikmuhs/ccache-action@v1.2.18
with:
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
+ - name: Use ccache on Debug builds only
+ if: inputs.build_type == 'Debug'
+ shell: bash
+ run: |
+ echo "CCACHE_VAR=ccache" >> $GITHUB_ENV
+
- name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
uses: actions/cache@v4.2.3
@@ -187,11 +181,16 @@ jobs:
ccache -p # Show config
ccache -z # Zero stats
- - name: Use ccache on Debug builds only
- if: inputs.build_type == 'Debug'
- shell: bash
+ - name: Configure ccache (Windows MSVC)
+ if: ${{ runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug' }}
run: |
- echo "CCACHE_VAR=ccache" >> $GITHUB_ENV
+ # 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
@@ -248,15 +247,22 @@ jobs:
arch: ${{ matrix.vcvars_arch }}
- name: Prepare AppImage (Linux)
- if: runner.os == 'Linux' && matrix.qt_ver != 5
+ if: runner.os == 'Linux'
+ env:
+ APPIMAGEUPDATE_HASH: ${{ matrix.appimageupdate_hash }}
+ LINUXDEPLOY_HASH: ${{ matrix.linuxdeploy_hash }}
+ LINUXDEPLOY_QT_HASH: ${{ matrix.linuxdeploy_qt_hash }}
run: |
- wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
- wget "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage"
- wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage"
+ wget "https://github.com/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/continuous/AppImageUpdate-x86_64.AppImage"
+ wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-x86_64.AppImage"
- sudo apt install libopengl0 libfuse2
+ 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'
@@ -274,15 +280,10 @@ jobs:
##
- name: Configure CMake (macOS)
- if: runner.os == 'macOS' && matrix.qt_ver == 6
+ if: runner.os == 'macOS'
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja
- - name: Configure CMake (macOS-Legacy)
- if: runner.os == 'macOS' && matrix.qt_ver == 5
- run: |
- cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -DCMAKE_OSX_ARCHITECTURES="x86_64" -G Ninja
-
- name: Configure CMake (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != ''
shell: msys2 {0}
@@ -290,19 +291,14 @@ jobs:
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 == ''
+ 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 }}
- # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix)
- if ("${{ env.CCACHE_VAR }}")
- {
- Copy-Item C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/ccache.exe -Destination C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/cl.exe
- echo "CLToolExe=cl.exe" >> $env:GITHUB_ENV
- echo "CLToolPath=C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/" >> $env:GITHUB_ENV
- echo "TrackFileAccess=false" >> $env:GITHUB_ENV
- }
- # Needed for ccache, but also speeds up compile
- echo "UseMultiToolTask=true" >> $env:GITHUB_ENV
- name: Configure CMake (Linux)
if: runner.os == 'Linux'
@@ -499,7 +495,7 @@ jobs:
}
- name: Package AppImage (Linux)
- if: runner.os == 'Linux' && matrix.qt_ver != 5
+ if: runner.os == 'Linux'
shell: bash
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
@@ -595,29 +591,22 @@ jobs:
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
+ - name: Upload binary tarball (Linux, portable)
+ if: runner.os == 'Linux'
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ runner.os }}-Qt6-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
path: PrismLauncher-portable.tar.gz
- name: Upload AppImage (Linux)
- if: runner.os == 'Linux' && matrix.qt_ver != 5
+ if: runner.os == 'Linux'
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage
- name: Upload AppImage Zsync (Linux)
- if: runner.os == 'Linux' && matrix.qt_ver != 5
+ if: runner.os == 'Linux'
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage.zsync
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index d1d810374..a5ac537f1 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -1,16 +1,49 @@
name: "CodeQL Code Scanning"
-on: [ push, pull_request, workflow_dispatch ]
+on:
+ push:
+ # NOTE: `!` doesn't work with `paths-ignore` :(
+ # So we a catch-all glob instead
+ # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674
+ paths:
+ - "**"
+ - "!.github/**"
+ - ".github/workflows/codeql.yml"
+ - "!flatpak/"
+ - "!nix/"
+ - "!scripts/"
+
+ - "!.git*"
+ - "!.envrc"
+ - "!**.md"
+ - "COPYING.md"
+ - "!renovate.json"
+ pull_request:
+ # See above
+ paths:
+ - "**"
+ - "!.github/**"
+ - ".github/workflows/codeql.yml"
+ - "!flatpak/"
+ - "!nix/"
+ - "!scripts/"
+
+ - "!.git*"
+ - "!.envrc"
+ - "!**.md"
+ - "COPYING.md"
+ - "!renovate.json"
+ workflow_dispatch:
jobs:
CodeQL:
runs-on: ubuntu-latest
-
+
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
- submodules: 'true'
+ submodules: "true"
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
@@ -20,14 +53,25 @@ jobs:
languages: cpp, java
- name: Install Dependencies
- run:
- sudo apt-get -y update
+ run: sudo apt-get -y update
- sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 libqt5networkauth5 libqt5networkauth5-dev libqt5opengl5 libqt5opengl5-dev
+ sudo apt-get -y install ninja-build extra-cmake-modules scdoc
+
+ - name: Install Qt
+ uses: jurplel/install-qt-action@v3
+ with:
+ aqtversion: "==3.1.*"
+ py7zrversion: ">=0.20.2"
+ version: "6.8.1"
+ host: "linux"
+ target: "desktop"
+ arch: ""
+ modules: "qt5compat qtimageformats qtnetworkauth"
+ tools: ""
- name: Configure and Build
run: |
- cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr -DLauncher_QT_VERSION_MAJOR=5 -G Ninja
+ cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr -G Ninja
cmake --build build
diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml
index 41cc2a51d..8caba46fa 100644
--- a/.github/workflows/flatpak.yml
+++ b/.github/workflows/flatpak.yml
@@ -2,22 +2,38 @@ name: Flatpak
on:
push:
- paths-ignore:
- - "**.md"
- - "**/LICENSE"
- - ".github/ISSUE_TEMPLATE/**"
- - ".markdownlint**"
- - "nix/**"
# We don't do anything with these artifacts on releases. They go to Flathub
tags-ignore:
- "*"
+ # NOTE: `!` doesn't work with `paths-ignore` :(
+ # So we a catch-all glob instead
+ # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674
+ paths:
+ - "**"
+ - "!.github/**"
+ - ".github/workflows/flatpak.yml"
+ - "!nix/"
+ - "!scripts/"
+
+ - "!.git*"
+ - "!.envrc"
+ - "!**.md"
+ - "COPYING.md"
+ - "!renovate.json"
pull_request:
- paths-ignore:
- - "**.md"
- - "**/LICENSE"
- - ".github/ISSUE_TEMPLATE/**"
- - ".markdownlint**"
- - "nix/**"
+ # See above
+ paths:
+ - "**"
+ - "!.github/**"
+ - ".github/workflows/flatpak.yml"
+ - "!nix/"
+ - "!scripts/"
+
+ - "!.git*"
+ - "!.envrc"
+ - "!**.md"
+ - "COPYING.md"
+ - "!renovate.json"
workflow_dispatch:
permissions:
diff --git a/.github/workflows/merge-blocking-pr.yml b/.github/workflows/merge-blocking-pr.yml
new file mode 100644
index 000000000..d37c33761
--- /dev/null
+++ b/.github/workflows/merge-blocking-pr.yml
@@ -0,0 +1,62 @@
+name: Merged Blocking Pull Request Automation
+
+on:
+ pull_request_target:
+ types:
+ - closed
+ workflow_dispatch:
+ inputs:
+ pr_id:
+ description: Local Pull Request number to work on
+ required: true
+ type: number
+
+jobs:
+ update-blocked-status:
+ name: Update Blocked Status
+ runs-on: ubuntu-latest
+
+ # a pr that was a `blocking:` label was merged.
+ # find the open pr's it was blocked by and trigger a refresh of their state
+ if: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'blocking') }}
+
+ steps:
+ - name: Generate token
+ id: generate-token
+ uses: actions/create-github-app-token@v2
+ with:
+ app-id: ${{ vars.PULL_REQUEST_APP_ID }}
+ private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }}
+
+ - name: Gather Dependent PRs
+ id: gather_deps
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ PR_NUMBER: ${{ inputs.pr_id || github.event.pull_request.number }}
+ run: |
+ blocked_prs=$(
+ gh -R ${{ github.repository }} pr list --label 'blocked' --json 'number,body' \
+ | jq -c --argjson pr "$PR_NUMBER" '
+ reduce ( .[] | select(
+ .body |
+ scan("(?:blocked (?:by|on)|stacked on):? #([0-9]+)") |
+ map(tonumber) |
+ any(.[]; . == $pr)
+ )) as $i ([]; . + [$i])
+ '
+ )
+ {
+ echo "deps=$blocked_prs"
+ echo "numdeps=$(jq -r '. | length' <<< "$blocked_prs")"
+ } >> "$GITHUB_OUTPUT"
+
+ - name: Trigger Blocked PR Workflows for Dependants
+ if: fromJSON(steps.gather_deps.outputs.numdeps) > 0
+ env:
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ DEPS: ${{ steps.gather_deps.outputs.deps }}
+ run: |
+ while read -r pr ; do
+ gh -R ${{ github.repository }} workflow run 'blocked-prs.yml' -r "${{ github.ref_name }}" -f pr_id="$pr"
+ done < <(jq -c '.[].number' <<< "$DEPS")
+
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
index 6c2b13de7..75ef7c65a 100644
--- a/.github/workflows/nix.yml
+++ b/.github/workflows/nix.yml
@@ -2,19 +2,36 @@ name: Nix
on:
push:
- paths-ignore:
- - "**.md"
- - "**/LICENSE"
- - ".github/ISSUE_TEMPLATE/**"
- - ".markdownlint**"
- - "flatpak/**"
+ tags:
+ - "*"
+ # NOTE: `!` doesn't work with `paths-ignore` :(
+ # So we a catch-all glob instead
+ # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674
+ paths:
+ - "**"
+ - "!.github/**"
+ - ".github/workflows/nix.yml"
+ - "!flatpak/"
+ - "!scripts/"
+
+ - "!.git*"
+ - "!.envrc"
+ - "!**.md"
+ - "COPYING.md"
+ - "!renovate.json"
pull_request_target:
- paths-ignore:
- - "**.md"
- - "**/LICENSE"
- - ".github/ISSUE_TEMPLATE/**"
- - ".markdownlint**"
- - "flatpak/**"
+ paths:
+ - "**"
+ - "!.github/**"
+ - ".github/workflows/nix.yml"
+ - "!flatpak/"
+ - "!scripts/"
+
+ - "!.git*"
+ - "!.envrc"
+ - "!**.md"
+ - "COPYING.md"
+ - "!renovate.json"
workflow_dispatch:
permissions:
@@ -50,17 +67,28 @@ jobs:
id-token: write
steps:
+ - name: Get merge commit
+ if: ${{ github.event_name == 'pull_request_target' }}
+ id: merge-commit
+ uses: PrismLauncher/PrismLauncher/.github/actions/get-merge-commit@develop
+ with:
+ pull-request-id: ${{ github.event.number }}
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
- name: Checkout repository
uses: actions/checkout@v4
+ with:
+ ref: ${{ steps.merge-commit.outputs.merge-commit-sha || github.sha }}
- name: Install Nix
- uses: DeterminateSystems/nix-installer-action@v16
+ uses: DeterminateSystems/nix-installer-action@v17
with:
determinate: ${{ env.USE_DETERMINATE }}
# For PRs
- name: Setup Nix Magic Cache
- if: ${{ env.USE_DETERMINATE }}
+ if: ${{ env.USE_DETERMINATE == 'true' }}
uses: DeterminateSystems/flakehub-cache-action@v1
# For in-tree builds
@@ -76,15 +104,18 @@ jobs:
nix flake check --print-build-logs --show-trace
- name: Build debug package
- if: ${{ env.DEBUG }}
+ if: ${{ env.DEBUG == 'true' }}
run: |
nix build \
--no-link --print-build-logs --print-out-paths \
.#prismlauncher-debug >> "$GITHUB_STEP_SUMMARY"
- name: Build release package
- if: ${{ !env.DEBUG }}
+ if: ${{ env.DEBUG == 'false' }}
+ env:
+ TAG: ${{ github.ref_name }}
+ SYSTEM: ${{ matrix.system }}
run: |
- nix build \
- --no-link --print-build-logs --print-out-paths \
- .#prismlauncher >> "$GITHUB_STEP_SUMMARY"
+ nix build --no-link --print-out-paths .#prismlauncher \
+ | tee -a "$GITHUB_STEP_SUMMARY" \
+ | xargs cachix pin prismlauncher "$TAG"-"$SYSTEM"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 034a8548b..8a7da812e 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -8,28 +8,6 @@ permissions:
contents: read
jobs:
- flakehub:
- name: FlakeHub
-
- runs-on: ubuntu-latest
-
- permissions:
- id-token: write
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- ref: ${{ github.ref }}
-
- - name: Install Nix
- uses: cachix/install-nix-action@v31
-
- - name: Publish on FlakeHub
- uses: determinatesystems/flakehub-push@v5
- with:
- visibility: "public"
-
winget:
name: Winget
diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml
index 9efafc8cc..e4c90ef0b 100644
--- a/.github/workflows/trigger_builds.yml
+++ b/.github/workflows/trigger_builds.yml
@@ -4,21 +4,39 @@ on:
push:
branches-ignore:
- "renovate/**"
- paths-ignore:
- - "**.md"
- - "**/LICENSE"
- - "flake.lock"
- - "packages/**"
- - ".github/ISSUE_TEMPLATE/**"
- - ".markdownlint**"
+ # NOTE: `!` doesn't work with `paths-ignore` :(
+ # So we a catch-all glob instead
+ # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674
+ paths:
+ - "**"
+ - "!.github/**"
+ - ".github/workflows/build.yml"
+ - ".github/workflows/trigger_builds.yml"
+ - "!flatpak/"
+ - "!nix/"
+ - "!scripts/"
+
+ - "!.git*"
+ - "!.envrc"
+ - "!**.md"
+ - "COPYING.md"
+ - "!renovate.json"
pull_request:
- paths-ignore:
- - "**.md"
- - "**/LICENSE"
- - "flake.lock"
- - "packages/**"
- - ".github/ISSUE_TEMPLATE/**"
- - ".markdownlint**"
+ # See above
+ paths:
+ - "**"
+ - "!.github/**"
+ - ".github/workflows/build.yml"
+ - ".github/workflows/trigger_builds.yml"
+ - "!flatpak/"
+ - "!nix/"
+ - "!scripts/"
+
+ - "!.git*"
+ - "!.envrc"
+ - "!**.md"
+ - "COPYING.md"
+ - "!renovate.json"
workflow_dispatch:
jobs:
diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml
index 134281b2c..96f616a43 100644
--- a/.github/workflows/trigger_release.yml
+++ b/.github/workflows/trigger_release.yml
@@ -46,10 +46,8 @@ jobs:
run: |
mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }}
mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz
- mv PrismLauncher-Linux-Qt5-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz
mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage
mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync
- mv PrismLauncher-macOS-Legacy*/PrismLauncher.zip PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip
mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip
tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}
@@ -90,7 +88,6 @@ jobs:
draft: true
prerelease: false
files: |
- PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz
PrismLauncher-Linux-x86_64.AppImage
PrismLauncher-Linux-x86_64.AppImage.zsync
PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz
@@ -104,5 +101,4 @@ jobs:
PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe
PrismLauncher-macOS-${{ env.VERSION }}.zip
- PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip
PrismLauncher-${{ env.VERSION }}.tar.gz
diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml
index 48a8418f5..f4b1c4f5d 100644
--- a/.github/workflows/update-flake.yml
+++ b/.github/workflows/update-flake.yml
@@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- - uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
+ - uses: cachix/install-nix-action@754537aaedb35f72ab11a60cc162c49ef3016495 # v31
- uses: DeterminateSystems/update-flake-lock@v24
with:
diff --git a/.gitignore b/.gitignore
index c8f056eef..b563afbc7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,8 +48,12 @@ run/
# Nix/NixOS
.direnv/
-.pre-commit-config.yaml
+## Used when manually invoking stdenv phases
+outputs/
+## Regular artifacts
result
+result-*
+repl-result-*
# Flatpak
.flatpak-builder
diff --git a/.gitmodules b/.gitmodules
index 0f437d277..0c56d8768 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,9 +4,6 @@
[submodule "libraries/tomlplusplus"]
path = libraries/tomlplusplus
url = https://github.com/marzer/tomlplusplus.git
-[submodule "libraries/filesystem"]
- path = libraries/filesystem
- url = https://github.com/gulrak/filesystem
[submodule "libraries/libnbtplusplus"]
path = libraries/libnbtplusplus
url = https://github.com/PrismLauncher/libnbtplusplus.git
@@ -22,3 +19,6 @@
[submodule "flatpak/shared-modules"]
path = flatpak/shared-modules
url = https://github.com/flathub/shared-modules.git
+[submodule "libraries/qt-qrcodegenerator/QR-Code-generator"]
+ path = libraries/qt-qrcodegenerator/QR-Code-generator
+ url = https://github.com/nayuki/QR-Code-generator
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 138049018..68d900c27 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -88,10 +88,8 @@ else()
endif()
endif()
-# Fix build with Qt 5.13
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y")
-
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_WARN_DEPRECATED_UP_TO=0x060200")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_UP_TO=0x060000")
# Fix aarch64 build for toml++
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0")
@@ -102,7 +100,7 @@ set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}"
# Export compile commands for debug builds if we can (useful in LSPs like clangd)
# https://cmake.org/cmake/help/v3.31/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html
if(CMAKE_GENERATOR STREQUAL "Unix Makefiles" OR CMAKE_GENERATOR STREQUAL "Ninja" AND CMAKE_BUILD_TYPE STREQUAL "Debug")
- set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
endif()
option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF)
@@ -195,10 +193,11 @@ set(Launcher_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE S
######## Set version numbers ########
set(Launcher_VERSION_MAJOR 10)
set(Launcher_VERSION_MINOR 0)
+set(Launcher_VERSION_PATCH 0)
-set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
-set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.0.0")
-set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},0,0")
+set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}")
+set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}.0")
+set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},${Launcher_VERSION_PATCH},0")
# Build platform.
set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.")
@@ -242,7 +241,7 @@ set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT ON)
# differing Linux/BSD/etc distributions. Downstream packagers should be explicitly opt-ing into this
# feature if they know it will work with their distribution.
if(UNIX AND NOT APPLE)
- set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF)
+ set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF)
endif()
# Java downloader
@@ -309,23 +308,7 @@ endif()
# Find the required Qt parts
include(QtVersionlessBackport)
-if(Launcher_QT_VERSION_MAJOR EQUAL 5)
- set(QT_VERSION_MAJOR 5)
- find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml NetworkAuth 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)
+if(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_VERSION_MAJOR 6)
find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth OpenGL)
find_package(Qt6 COMPONENTS DBus)
@@ -343,29 +326,16 @@ else()
message(FATAL_ERROR "Qt version ${Launcher_QT_VERSION_MAJOR} is not supported")
endif()
-if(Launcher_QT_VERSION_MAJOR EQUAL 5)
- include(ECMQueryQt)
- ecm_query_qt(QT_PLUGINS_DIR QT_INSTALL_PLUGINS)
- ecm_query_qt(QT_LIBS_DIR QT_INSTALL_LIBS)
- ecm_query_qt(QT_LIBEXECS_DIR QT_INSTALL_LIBEXECS)
-else()
+if(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_PLUGINS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_PLUGINS})
set(QT_LIBS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBS})
set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS})
endif()
-# NOTE: Qt 6 already sets this by default
-if (Qt5_POSITION_INDEPENDENT_CODE)
- SET(CMAKE_POSITION_INDEPENDENT_CODE ON)
-endif()
-
if(NOT Launcher_FORCE_BUNDLED_LIBS)
# Find toml++
find_package(tomlplusplus 3.2.0 QUIET)
- # Find ghc_filesystem
- find_package(ghc_filesystem QUIET)
-
# Find cmark
find_package(cmark QUIET)
endif()
@@ -383,7 +353,7 @@ set(Launcher_ENABLE_UPDATER NO)
set(Launcher_BUILD_UPDATER NO)
if (NOT APPLE AND (NOT Launcher_UPDATER_GITHUB_REPO STREQUAL "" AND NOT Launcher_BUILD_ARTIFACT STREQUAL ""))
- set(Launcher_BUILD_UPDATER YES)
+ set(Launcher_BUILD_UPDATER YES)
endif()
if(NOT (UNIX AND APPLE))
@@ -505,6 +475,7 @@ add_subdirectory(libraries/libnbtplusplus)
add_subdirectory(libraries/systeminfo) # system information library
add_subdirectory(libraries/launcher) # java based launcher part for Minecraft
add_subdirectory(libraries/javacheck) # java compatibility checker
+add_subdirectory(libraries/qt-qrcodegenerator) # qr code generator
if(FORCE_BUNDLED_ZLIB)
message(STATUS "Using bundled zlib")
@@ -560,12 +531,6 @@ else()
endif()
add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
-if (NOT ghc_filesystem_FOUND)
- message(STATUS "Using bundled ghc_filesystem")
- add_subdirectory(libraries/filesystem) # Implementation of std::filesystem for old C++, for usage in old macOS
-else()
- message(STATUS "Using system ghc_filesystem")
-endif()
add_subdirectory(libraries/qdcss) # css parser
############################### Built Artifacts ###############################
diff --git a/COPYING.md b/COPYING.md
index 0ea3437d3..1ebde116f 100644
--- a/COPYING.md
+++ b/COPYING.md
@@ -108,7 +108,7 @@
Information on third party licenses used in MinGW-w64 can be found in its COPYING.MinGW-w64-runtime.txt.
-## Qt 5/6
+## Qt 6
Copyright (C) 2022 The Qt Company Ltd and other contributors.
Contact: https://www.qt.io/licensing
@@ -362,28 +362,6 @@
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
-## gulrak/filesystem
-
- Copyright (c) 2018, Steffen Schümann
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
-
## Breeze icons
Copyright (C) 2014 Uri Herrera and others
@@ -425,3 +403,12 @@
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see .
+
+## qt-qrcodegenerator (`libraries/qt-qrcodegenerator`)
+
+ Copyright © 2024 Project Nayuki. (MIT License)
+ https://www.nayuki.io/page/qr-code-generator-library
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+ - The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in
index 2124d02ae..6bebcb80e 100644
--- a/buildconfig/BuildConfig.cpp.in
+++ b/buildconfig/BuildConfig.cpp.in
@@ -34,8 +34,8 @@
*/
#include
-#include "BuildConfig.h"
#include
+#include "BuildConfig.h"
const Config BuildConfig;
@@ -58,6 +58,7 @@ Config::Config()
// Version information
VERSION_MAJOR = @Launcher_VERSION_MAJOR@;
VERSION_MINOR = @Launcher_VERSION_MINOR@;
+ VERSION_PATCH = @Launcher_VERSION_PATCH@;
BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@";
BUILD_ARTIFACT = "@Launcher_BUILD_ARTIFACT@";
@@ -74,14 +75,13 @@ Config::Config()
MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@";
MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@";
- if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty())
- {
+ if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) {
UPDATER_ENABLED = true;
- } else if(!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) {
+ } else if (!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) {
UPDATER_ENABLED = true;
}
- #cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER
+#cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER
JAVA_DOWNLOADER_ENABLED = Launcher_ENABLE_JAVA_DOWNLOADER;
GIT_COMMIT = "@Launcher_GIT_COMMIT@";
@@ -89,27 +89,19 @@ Config::Config()
GIT_REFSPEC = "@Launcher_GIT_REFSPEC@";
// Assume that builds outside of Git repos are "stable"
- if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND")
- || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND")
- || GIT_REFSPEC == QStringLiteral("")
- || GIT_TAG == QStringLiteral("GIT-NOTFOUND"))
- {
+ if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") ||
+ GIT_REFSPEC == QStringLiteral("") || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) {
GIT_REFSPEC = "refs/heads/stable";
GIT_TAG = versionString();
GIT_COMMIT = "";
}
- if (GIT_REFSPEC.startsWith("refs/heads/"))
- {
+ if (GIT_REFSPEC.startsWith("refs/heads/")) {
VERSION_CHANNEL = GIT_REFSPEC;
- VERSION_CHANNEL.remove("refs/heads/");
- }
- else if (!GIT_COMMIT.isEmpty())
- {
+ VERSION_CHANNEL.remove("refs/heads/");
+ } else if (!GIT_COMMIT.isEmpty()) {
VERSION_CHANNEL = GIT_COMMIT.mid(0, 8);
- }
- else
- {
+ } else {
VERSION_CHANNEL = "unknown";
}
@@ -136,7 +128,7 @@ Config::Config()
QString Config::versionString() const
{
- return QString("%1.%2").arg(VERSION_MAJOR).arg(VERSION_MINOR);
+ return QString("%1.%2.%3").arg(VERSION_MAJOR).arg(VERSION_MINOR).arg(VERSION_PATCH);
}
QString Config::printableVersionString() const
@@ -144,8 +136,7 @@ QString Config::printableVersionString() const
QString vstr = versionString();
// If the build is not a main release, append the channel
- if(VERSION_CHANNEL != "stable" && GIT_TAG != vstr)
- {
+ if (VERSION_CHANNEL != "stable" && GIT_TAG != vstr) {
vstr += "-" + VERSION_CHANNEL;
}
return vstr;
@@ -162,4 +153,3 @@ QString Config::systemID() const
{
return QStringLiteral("%1 %2 %3").arg(COMPILER_TARGET_SYSTEM, COMPILER_TARGET_SYSTEM_VERSION, COMPILER_TARGET_SYSTEM_PROCESSOR);
}
-
diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h
index 099d9b5ca..b59adcb57 100644
--- a/buildconfig/BuildConfig.h
+++ b/buildconfig/BuildConfig.h
@@ -59,6 +59,8 @@ class Config {
int VERSION_MAJOR;
/// The minor version number.
int VERSION_MINOR;
+ /// The patch version number.
+ int VERSION_PATCH;
/**
* The version channel
diff --git a/default.nix b/default.nix
index 6466507b7..5ecef5590 100644
--- a/default.nix
+++ b/default.nix
@@ -1,9 +1,4 @@
-(import (
- let
- lock = builtins.fromJSON (builtins.readFile ./flake.lock);
- in
- fetchTarball {
- url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
- sha256 = lock.nodes.flake-compat.locked.narHash;
- }
-) { src = ./.; }).defaultNix
+(import (fetchTarball {
+ url = "https://github.com/edolstra/flake-compat/archive/ff81ac966bb2cae68946d5ed5fc4994f96d0ffec.tar.gz";
+ sha256 = "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=";
+}) { src = ./.; }).defaultNix
diff --git a/flake.lock b/flake.lock
index ef4c9f555..07fa5117a 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,29 +1,13 @@
{
"nodes": {
- "flake-compat": {
- "flake": false,
- "locked": {
- "lastModified": 1733328505,
- "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
- "owner": "edolstra",
- "repo": "flake-compat",
- "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
- "type": "github"
- },
- "original": {
- "owner": "edolstra",
- "repo": "flake-compat",
- "type": "github"
- }
- },
"libnbtplusplus": {
"flake": false,
"locked": {
- "lastModified": 1699286814,
- "narHash": "sha256-yy0q+bky80LtK1GWzz7qpM+aAGrOqLuewbid8WT1ilk=",
+ "lastModified": 1744811532,
+ "narHash": "sha256-qhmjaRkt+O7A+gu6HjUkl7QzOEb4r8y8vWZMG2R/C6o=",
"owner": "PrismLauncher",
"repo": "libnbtplusplus",
- "rev": "23b955121b8217c1c348a9ed2483167a6f3ff4ad",
+ "rev": "531449ba1c930c98e0bcf5d332b237a8566f9d78",
"type": "github"
},
"original": {
@@ -32,28 +16,13 @@
"type": "github"
}
},
- "nix-filter": {
- "locked": {
- "lastModified": 1731533336,
- "narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
- "owner": "numtide",
- "repo": "nix-filter",
- "rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
- "type": "github"
- },
- "original": {
- "owner": "numtide",
- "repo": "nix-filter",
- "type": "github"
- }
- },
"nixpkgs": {
"locked": {
- "lastModified": 1742422364,
- "narHash": "sha256-mNqIplmEohk5jRkqYqG19GA8MbQ/D4gQSK0Mu4LvfRQ=",
+ "lastModified": 1744932701,
+ "narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "a84ebe20c6bc2ecbcfb000a50776219f48d134cc",
+ "rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef",
"type": "github"
},
"original": {
@@ -63,12 +32,27 @@
"type": "github"
}
},
+ "qt-qrcodegenerator": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1737616857,
+ "narHash": "sha256-6SugPt0lp1Gz7nV23FLmsmpfzgFItkSw7jpGftsDPWc=",
+ "owner": "nayuki",
+ "repo": "QR-Code-generator",
+ "rev": "2c9044de6b049ca25cb3cd1649ed7e27aa055138",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nayuki",
+ "repo": "QR-Code-generator",
+ "type": "github"
+ }
+ },
"root": {
"inputs": {
- "flake-compat": "flake-compat",
"libnbtplusplus": "libnbtplusplus",
- "nix-filter": "nix-filter",
- "nixpkgs": "nixpkgs"
+ "nixpkgs": "nixpkgs",
+ "qt-qrcodegenerator": "qt-qrcodegenerator"
}
}
},
diff --git a/flake.nix b/flake.nix
index 150240c8b..69abd78dd 100644
--- a/flake.nix
+++ b/flake.nix
@@ -16,25 +16,8 @@
flake = false;
};
- nix-filter.url = "github:numtide/nix-filter";
-
- /*
- Inputs below this are optional and can be removed
-
- ```
- {
- inputs.prismlauncher = {
- url = "github:PrismLauncher/PrismLauncher";
- inputs = {
- flake-compat.follows = "";
- };
- };
- }
- ```
- */
-
- flake-compat = {
- url = "github:edolstra/flake-compat";
+ qt-qrcodegenerator = {
+ url = "github:nayuki/QR-Code-generator";
flake = false;
};
};
@@ -44,9 +27,9 @@
self,
nixpkgs,
libnbtplusplus,
- nix-filter,
- ...
+ qt-qrcodegenerator,
}:
+
let
inherit (nixpkgs) lib;
@@ -58,37 +41,129 @@
forAllSystems = lib.genAttrs systems;
nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system});
in
+
{
checks = forAllSystems (
system:
+
let
- checks' = nixpkgsFor.${system}.callPackage ./nix/checks.nix { inherit self; };
+ pkgs = nixpkgsFor.${system};
+ llvm = pkgs.llvmPackages_19;
in
- lib.filterAttrs (_: lib.isDerivation) checks'
+
+ {
+ formatting =
+ pkgs.runCommand "check-formatting"
+ {
+ nativeBuildInputs = with pkgs; [
+ deadnix
+ llvm.clang-tools
+ markdownlint-cli
+ nixfmt-rfc-style
+ statix
+ ];
+ }
+ ''
+ cd ${self}
+
+ echo "Running clang-format...."
+ clang-format --dry-run --style='file' --Werror */**.{c,cc,cpp,h,hh,hpp}
+
+ echo "Running deadnix..."
+ deadnix --fail
+
+ echo "Running markdownlint..."
+ markdownlint --dot .
+
+ echo "Running nixfmt..."
+ find -type f -name '*.nix' -exec nixfmt --check {} +
+
+ echo "Running statix"
+ statix check .
+
+ touch $out
+ '';
+ }
);
devShells = forAllSystems (
system:
+
let
pkgs = nixpkgsFor.${system};
+ llvm = pkgs.llvmPackages_19;
+
+ packages' = self.packages.${system};
+
+ welcomeMessage = ''
+ Welcome to the Prism Launcher repository! 🌈
+
+ We just set some things up for you. To get building, you can run:
+
+ ```
+ $ cd "$cmakeBuildDir"
+ $ ninjaBuildPhase
+ $ ninjaInstallPhase
+ ```
+
+ Feel free to ask any questions in our Discord server or Matrix space:
+ - https://prismlauncher.org/discord
+ - https://matrix.to/#/#prismlauncher:matrix.org
+
+ And thanks for helping out :)
+ '';
+
+ # Re-use our package wrapper to wrap our development environment
+ qt-wrapper-env = packages'.prismlauncher.overrideAttrs (old: {
+ name = "qt-wrapper-env";
+
+ # Required to use script-based makeWrapper below
+ strictDeps = true;
+
+ # We don't need/want the unwrapped Prism package
+ paths = [ ];
+
+ nativeBuildInputs = old.nativeBuildInputs or [ ] ++ [
+ # Ensure the wrapper is script based so it can be sourced
+ pkgs.makeWrapper
+ ];
+
+ # Inspired by https://discourse.nixos.org/t/python-qt-woes/11808/10
+ buildCommand = ''
+ makeQtWrapper ${lib.getExe pkgs.runtimeShellPackage} "$out"
+ sed -i '/^exec/d' "$out"
+ '';
+ });
in
+
{
default = pkgs.mkShell {
- inputsFrom = [ self.packages.${system}.prismlauncher-unwrapped ];
- buildInputs = with pkgs; [
+ name = "prism-launcher";
+
+ inputsFrom = [ packages'.prismlauncher-unwrapped ];
+
+ packages = with pkgs; [
ccache
- ninja
- llvmPackages_19.clang-tools
+ llvm.clang-tools
];
- cmakeFlags = self.packages.${system}.prismlauncher-unwrapped.cmakeFlags ++ [
- "-GNinja"
- "-Bbuild"
- ];
+ cmakeBuildType = "Debug";
+ cmakeFlags = [ "-GNinja" ] ++ packages'.prismlauncher.cmakeFlags;
+ dontFixCmake = true;
shellHook = ''
- cmake $cmakeFlags -D CMAKE_BUILD_TYPE=Debug
- ln -s {build/,}compile_commands.json
+ echo "Sourcing ${qt-wrapper-env}"
+ source ${qt-wrapper-env}
+
+ git submodule update --init --force
+
+ if [ ! -f compile_commands.json ]; then
+ cmakeConfigurePhase
+ cd ..
+ ln -s "$cmakeBuildDir"/compile_commands.json compile_commands.json
+ fi
+
+ echo ${lib.escapeShellArg welcomeMessage}
'';
};
}
@@ -100,7 +175,7 @@
prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix {
inherit
libnbtplusplus
- nix-filter
+ qt-qrcodegenerator
self
;
};
@@ -110,6 +185,7 @@
packages = forAllSystems (
system:
+
let
pkgs = nixpkgsFor.${system};
@@ -122,6 +198,7 @@
default = prismPackages.prismlauncher;
};
in
+
# Only output them if they're available on the current system
lib.filterAttrs (_: lib.meta.availableOn pkgs.stdenv.hostPlatform) packages
);
@@ -129,16 +206,18 @@
# We put these under legacyPackages as they are meant for CI, not end user consumption
legacyPackages = forAllSystems (
system:
+
let
- prismPackages = self.packages.${system};
- legacyPackages = self.legacyPackages.${system};
+ packages' = self.packages.${system};
+ legacyPackages' = self.legacyPackages.${system};
in
+
{
- prismlauncher-debug = prismPackages.prismlauncher.override {
- prismlauncher-unwrapped = legacyPackages.prismlauncher-unwrapped-debug;
+ prismlauncher-debug = packages'.prismlauncher.override {
+ prismlauncher-unwrapped = legacyPackages'.prismlauncher-unwrapped-debug;
};
- prismlauncher-unwrapped-debug = prismPackages.prismlauncher-unwrapped.overrideAttrs {
+ prismlauncher-unwrapped-debug = packages'.prismlauncher-unwrapped.overrideAttrs {
cmakeBuildType = "Debug";
dontStrip = true;
};
diff --git a/flatpak/shared-modules b/flatpak/shared-modules
index f5d368a31..73f08ed2c 160000
--- a/flatpak/shared-modules
+++ b/flatpak/shared-modules
@@ -1 +1 @@
-Subproject commit f5d368a31d6ef046eb2955c74ec6f54f32ed5c4e
+Subproject commit 73f08ed2c3187f6648ca04ebef030930a6c9f0be
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index 6cd2d5312..0449a7055 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -96,6 +96,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -153,11 +154,16 @@
#endif
#if defined Q_OS_WIN32
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
#include
#include
-#include "WindowsConsole.h"
+#include "console/WindowsConsole.h"
#endif
+#include "console/Console.h"
+
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
@@ -165,6 +171,63 @@ static const QLatin1String liveCheckFile("live.check");
PixmapCache* PixmapCache::s_instance = nullptr;
+static bool isANSIColorConsole;
+
+static QString defaultLogFormat = QStringLiteral(
+ "%{time process}"
+ " "
+ "%{if-debug}Debug:%{endif}"
+ "%{if-info}Info:%{endif}"
+ "%{if-warning}Warning:%{endif}"
+ "%{if-critical}Critical:%{endif}"
+ "%{if-fatal}Fatal:%{endif}"
+ " "
+ "%{if-category}[%{category}] %{endif}"
+ "%{message}"
+ " "
+ "(%{function}:%{line})");
+
+#define ansi_reset "\x1b[0m"
+#define ansi_bold "\x1b[1m"
+#define ansi_reset_bold "\x1b[22m"
+#define ansi_faint "\x1b[2m"
+#define ansi_italic "\x1b[3m"
+#define ansi_red_fg "\x1b[31m"
+#define ansi_green_fg "\x1b[32m"
+#define ansi_yellow_fg "\x1b[33m"
+#define ansi_blue_fg "\x1b[34m"
+#define ansi_purple_fg "\x1b[35m"
+#define ansi_inverse "\x1b[7m"
+
+// clang-format off
+static QString ansiLogFormat = QStringLiteral(
+ ansi_faint "%{time process}" ansi_reset
+ " "
+ "%{if-debug}" ansi_bold ansi_green_fg "D:" ansi_reset "%{endif}"
+ "%{if-info}" ansi_bold ansi_blue_fg "I:" ansi_reset "%{endif}"
+ "%{if-warning}" ansi_bold ansi_yellow_fg "W:" ansi_reset_bold "%{endif}"
+ "%{if-critical}" ansi_bold ansi_red_fg "C:" ansi_reset_bold "%{endif}"
+ "%{if-fatal}" ansi_bold ansi_inverse ansi_red_fg "F:" ansi_reset_bold "%{endif}"
+ " "
+ "%{if-category}" ansi_bold "[%{category}]" ansi_reset_bold " %{endif}"
+ "%{message}"
+ " "
+ ansi_reset ansi_faint "(%{function}:%{line})" ansi_reset
+);
+// clang-format on
+
+#undef ansi_inverse
+#undef ansi_purple_fg
+#undef ansi_blue_fg
+#undef ansi_yellow_fg
+#undef ansi_green_fg
+#undef ansi_red_fg
+#undef ansi_italic
+#undef ansi_faint
+#undef ansi_bold
+#undef ansi_reset_bold
+#undef ansi_reset
+
namespace {
/** This is used so that we can output to the log file in addition to the CLI. */
@@ -173,11 +236,24 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt
static std::mutex loggerMutex;
const std::lock_guard lock(loggerMutex); // synchronized, QFile logFile is not thread-safe
+ if (isANSIColorConsole) {
+ // ensure default is set for log file
+ qSetMessagePattern(defaultLogFormat);
+ }
+
QString out = qFormatLogMessage(type, context, msg);
out += QChar::LineFeed;
APPLICATION->logFile->write(out.toUtf8());
APPLICATION->logFile->flush();
+
+ if (isANSIColorConsole) {
+ // format ansi for console;
+ qSetMessagePattern(ansiLogFormat);
+ out = qFormatLogMessage(type, context, msg);
+ out += QChar::LineFeed;
+ }
+
QTextStream(stderr) << out.toLocal8Bit();
fflush(stderr);
}
@@ -218,8 +294,18 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// attach the parent console if stdout not already captured
if (AttachWindowsConsole()) {
consoleAttached = true;
+ if (auto err = EnableAnsiSupport(); !err) {
+ isANSIColorConsole = true;
+ } else {
+ std::cout << "Error setting up ansi console" << err.message() << std::endl;
+ }
+ }
+#else
+ if (console::isConsole()) {
+ isANSIColorConsole = true;
}
#endif
+
setOrganizationName(BuildConfig.LAUNCHER_NAME);
setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN);
setApplicationName(BuildConfig.LAUNCHER_NAME);
@@ -375,19 +461,20 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_peerInstance = new LocalPeer(this, appID);
connect(m_peerInstance, &LocalPeer::messageReceived, this, &Application::messageReceived);
if (m_peerInstance->isClient()) {
+ bool sentMessage = false;
int timeout = 2000;
if (m_instanceIdToLaunch.isEmpty()) {
ApplicationMessage activate;
activate.command = "activate";
- m_peerInstance->sendMessage(activate.serialize(), timeout);
+ sentMessage = m_peerInstance->sendMessage(activate.serialize(), timeout);
if (!m_urlsToImport.isEmpty()) {
for (auto url : m_urlsToImport) {
ApplicationMessage import;
import.command = "import";
import.args.insert("url", url.toString());
- m_peerInstance->sendMessage(import.serialize(), timeout);
+ sentMessage = m_peerInstance->sendMessage(import.serialize(), timeout);
}
}
} else {
@@ -407,10 +494,16 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
launch.args["offline_enabled"] = "true";
launch.args["offline_name"] = m_offlineName;
}
- m_peerInstance->sendMessage(launch.serialize(), timeout);
+ sentMessage = m_peerInstance->sendMessage(launch.serialize(), timeout);
+ }
+ if (sentMessage) {
+ m_status = Application::Succeeded;
+ return;
+ } else {
+ std::cerr << "Unable to redirect command to already running instance\n";
+ // C function not Qt function - event loop not started yet
+ ::exit(1);
}
- m_status = Application::Succeeded;
- return;
}
}
@@ -441,27 +534,14 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
return;
}
qInstallMessageHandler(appDebugOutput);
-
- qSetMessagePattern(
- "%{time process}"
- " "
- "%{if-debug}D%{endif}"
- "%{if-info}I%{endif}"
- "%{if-warning}W%{endif}"
- "%{if-critical}C%{endif}"
- "%{if-fatal}F%{endif}"
- " "
- "|"
- " "
- "%{if-category}[%{category}]: %{endif}"
- "%{message}");
+ qSetMessagePattern(defaultLogFormat);
bool foundLoggingRules = false;
auto logRulesFile = QStringLiteral("qtlogging.ini");
auto logRulesPath = FS::PathCombine(dataPath, logRulesFile);
- qDebug() << "Testing" << logRulesPath << "...";
+ qInfo() << "Testing" << logRulesPath << "...";
foundLoggingRules = QFile::exists(logRulesPath);
// search the dataPath()
@@ -469,7 +549,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) {
logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile));
if (!logRulesPath.isEmpty()) {
- qDebug() << "Found" << logRulesPath << "...";
+ qInfo() << "Found" << logRulesPath << "...";
foundLoggingRules = true;
}
}
@@ -480,28 +560,28 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
#else
logRulesPath = FS::PathCombine(m_rootPath, logRulesFile);
#endif
- qDebug() << "Testing" << logRulesPath << "...";
+ qInfo() << "Testing" << logRulesPath << "...";
foundLoggingRules = QFile::exists(logRulesPath);
}
if (foundLoggingRules) {
// load and set logging rules
- qDebug() << "Loading logging rules from:" << logRulesPath;
+ qInfo() << "Loading logging rules from:" << logRulesPath;
QSettings loggingRules(logRulesPath, QSettings::IniFormat);
loggingRules.beginGroup("Rules");
QStringList rule_names = loggingRules.childKeys();
QStringList rules;
- qDebug() << "Setting log rules:";
+ qInfo() << "Setting log rules:";
for (auto rule_name : rule_names) {
auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString());
rules.append(rule);
- qDebug() << " " << rule;
+ qInfo() << " " << rule;
}
auto rules_str = rules.join("\n");
QLoggingCategory::setFilterRules(rules_str);
}
- qDebug() << "<> Log initialized.";
+ qInfo() << "<> Log initialized.";
}
{
@@ -518,33 +598,33 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
}
{
- qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
- qDebug() << "Version : " << BuildConfig.printableVersionString();
- qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM;
- qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
- qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
- qDebug() << "Compiled for : " << BuildConfig.systemID();
- qDebug() << "Compiled by : " << BuildConfig.compilerID();
- qDebug() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT;
- qDebug() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No");
+ qInfo() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
+ qInfo() << "Version : " << BuildConfig.printableVersionString();
+ qInfo() << "Platform : " << BuildConfig.BUILD_PLATFORM;
+ qInfo() << "Git commit : " << BuildConfig.GIT_COMMIT;
+ qInfo() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
+ qInfo() << "Compiled for : " << BuildConfig.systemID();
+ qInfo() << "Compiled by : " << BuildConfig.compilerID();
+ qInfo() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT;
+ qInfo() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No");
if (adjustedBy.size()) {
- qDebug() << "Work dir before adjustment : " << origcwdPath;
- qDebug() << "Work dir after adjustment : " << QDir::currentPath();
- qDebug() << "Adjusted by : " << adjustedBy;
+ qInfo() << "Work dir before adjustment : " << origcwdPath;
+ qInfo() << "Work dir after adjustment : " << QDir::currentPath();
+ qInfo() << "Adjusted by : " << adjustedBy;
} else {
- qDebug() << "Work dir : " << QDir::currentPath();
+ qInfo() << "Work dir : " << QDir::currentPath();
}
- qDebug() << "Binary path : " << binPath;
- qDebug() << "Application root path : " << m_rootPath;
+ qInfo() << "Binary path : " << binPath;
+ qInfo() << "Application root path : " << m_rootPath;
if (!m_instanceIdToLaunch.isEmpty()) {
- qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch;
+ qInfo() << "ID of instance to launch : " << m_instanceIdToLaunch;
}
if (!m_serverToJoin.isEmpty()) {
- qDebug() << "Address of server to join :" << m_serverToJoin;
+ qInfo() << "Address of server to join :" << m_serverToJoin;
} else if (!m_worldToJoin.isEmpty()) {
- qDebug() << "Name of the world to join :" << m_worldToJoin;
+ qInfo() << "Name of the world to join :" << m_worldToJoin;
}
- qDebug() << "<> Paths set.";
+ qInfo() << "<> Paths set.";
}
if (m_liveCheck) {
@@ -709,7 +789,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("ToolbarsLocked", false);
+ // Instance
m_settings->registerSetting("InstSortMode", "Name");
+ m_settings->registerSetting("InstRenamingMode", "AskEverytime");
m_settings->registerSetting("SelectedInstance", QString());
// Window state and geometry
@@ -814,7 +896,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
PixmapCache::setInstance(new PixmapCache(this));
- qDebug() << "<> Settings loaded.";
+ qInfo() << "<> Settings loaded.";
}
#ifndef QT_NO_ACCESSIBILITY
@@ -830,7 +912,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
QString user = settings()->get("ProxyUser").toString();
QString pass = settings()->get("ProxyPass").toString();
updateProxySettings(proxyTypeStr, addr, port, user, pass);
- qDebug() << "<> Network done.";
+ qInfo() << "<> Network done.";
}
// load translations
@@ -838,8 +920,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_translations.reset(new TranslationsModel("translations"));
auto bcp47Name = m_settings->get("Language").toString();
m_translations->selectLanguage(bcp47Name);
- qDebug() << "Your language is" << bcp47Name;
- qDebug() << "<> Translations loaded.";
+ qInfo() << "Your language is" << bcp47Name;
+ qInfo() << "<> Translations loaded.";
}
// Instance icons
@@ -850,7 +932,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_icons.reset(new IconList(instFolders, setting->get().toString()));
connect(setting.get(), &Setting::SettingChanged,
[this](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); });
- qDebug() << "<> Instance icons initialized.";
+ qInfo() << "<> Instance icons initialized.";
}
// Themes
@@ -862,25 +944,25 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// instance path: check for problems with '!' in instance path and warn the user in the log
// and remember that we have to show him a dialog when the gui starts (if it does so)
QString instDir = InstDirSetting->get().toString();
- qDebug() << "Instance path : " << instDir;
+ qInfo() << "Instance path : " << instDir;
if (FS::checkProblemticPathJava(QDir(instDir))) {
qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!";
}
m_instances.reset(new InstanceList(m_settings, instDir, this));
connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged);
- qDebug() << "Loading Instances...";
+ qInfo() << "Loading Instances...";
m_instances->loadList();
- qDebug() << "<> Instances loaded.";
+ qInfo() << "<> Instances loaded.";
}
// and accounts
{
m_accounts.reset(new AccountList(this));
- qDebug() << "Loading accounts...";
+ qInfo() << "Loading accounts...";
m_accounts->setListFilePath("accounts.json", true);
m_accounts->loadList();
m_accounts->fillQueue();
- qDebug() << "<> Accounts loaded.";
+ qInfo() << "<> Accounts loaded.";
}
// init the http meta cache
@@ -901,7 +983,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_metacache->addBase("meta", QDir("meta").absolutePath());
m_metacache->addBase("java", QDir("cache/java").absolutePath());
m_metacache->Load();
- qDebug() << "<> Cache initialized.";
+ qInfo() << "<> Cache initialized.";
}
// now we have network, download translation updates
diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp
index ccfd0b847..70e0f9dc1 100644
--- a/launcher/BaseInstance.cpp
+++ b/launcher/BaseInstance.cpp
@@ -42,8 +42,8 @@
#include
#include
#include
-#include
+#include "Application.h"
#include "settings/INISettingsObject.h"
#include "settings/OverrideSetting.h"
#include "settings/Setting.h"
@@ -174,6 +174,12 @@ void BaseInstance::copyManagedPack(BaseInstance& other)
m_settings->set("ManagedPackName", other.getManagedPackName());
m_settings->set("ManagedPackVersionID", other.getManagedPackVersionID());
m_settings->set("ManagedPackVersionName", other.getManagedPackVersionName());
+
+ if (APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() && m_settings->get("AutomaticJava").toBool() &&
+ m_settings->get("OverrideJavaLocation").toBool()) {
+ m_settings->set("OverrideJavaLocation", false);
+ m_settings->set("JavaPath", "");
+ }
}
int BaseInstance::getConsoleMaxLines() const
@@ -386,6 +392,12 @@ void BaseInstance::setName(QString val)
emit propertiesChanged(this);
}
+bool BaseInstance::syncInstanceDirName(const QString& newRoot) const
+{
+ auto oldRoot = instanceRoot();
+ return oldRoot == newRoot || QFile::rename(oldRoot, newRoot);
+}
+
QString BaseInstance::name() const
{
return m_settings->get("name").toString();
diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h
index 9827a08b4..1acf1afe0 100644
--- a/launcher/BaseInstance.h
+++ b/launcher/BaseInstance.h
@@ -126,6 +126,9 @@ class BaseInstance : public QObject, public std::enable_shared_from_this::finished, this, &DataMigrationTask::dryRunFinished);
disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::dryRunAborted);
-#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
if (!m_copyFuture.isValid() || !m_copyFuture.result()) {
-#else
- if (!m_copyFuture.result()) {
-#endif
emitFailed(tr("Failed to scan source path."));
return;
}
@@ -75,11 +71,7 @@ void DataMigrationTask::copyFinished()
disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished);
disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::copyAborted);
-#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
if (!m_copyFuture.isValid() || !m_copyFuture.result()) {
-#else
- if (!m_copyFuture.result()) {
-#endif
emitFailed(tr("Some paths could not be copied!"));
return;
}
diff --git a/launcher/FileIgnoreProxy.cpp b/launcher/FileIgnoreProxy.cpp
index 89c91ec1d..0314057d1 100644
--- a/launcher/FileIgnoreProxy.cpp
+++ b/launcher/FileIgnoreProxy.cpp
@@ -282,11 +282,7 @@ void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName)
}
auto ignoreData = ignoreFile.readAll();
auto string = QString::fromUtf8(ignoreData);
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
setBlockedPaths(string.split('\n', Qt::SkipEmptyParts));
-#else
- setBlockedPaths(string.split('\n', QString::SkipEmptyParts));
-#endif
}
void FileIgnoreProxy::saveBlockedPathsToFile(const QString& fileName)
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
index 954e7936e..08dc7d2cc 100644
--- a/launcher/FileSystem.cpp
+++ b/launcher/FileSystem.cpp
@@ -77,24 +77,8 @@
#include
#endif
-// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
-
-#ifdef __APPLE__
-#include // for deployment target to support pre-catalina targets without std::fs
-#endif // __APPLE__
-
-#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
-#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
-#define GHC_USE_STD_FS
#include
namespace fs = std::filesystem;
-#endif // MacOS min version check
-#endif // Other OSes version check
-
-#ifndef GHC_USE_STD_FS
-#include
-namespace fs = ghc::filesystem;
-#endif
// clone
#if defined(Q_OS_LINUX)
@@ -695,9 +679,6 @@ bool deletePath(QString path)
bool trash(QString path, QString* pathInTrash)
{
-#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
- return false;
-#else
// FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal
if (DesktopServices::isFlatpak())
return false;
@@ -706,7 +687,6 @@ bool trash(QString path, QString* pathInTrash)
return false;
#endif
return QFile::moveToTrash(path, pathInTrash);
-#endif
}
QString PathCombine(const QString& path1, const QString& path2)
@@ -740,11 +720,7 @@ int pathDepth(const QString& path)
QFileInfo info(path);
-#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
- auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), QString::SkipEmptyParts);
-#else
auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), Qt::SkipEmptyParts);
-#endif
int numParts = parts.length();
numParts -= parts.count(".");
@@ -764,11 +740,7 @@ QString pathTruncate(const QString& path, int depth)
return pathTruncate(trunc, depth);
}
-#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
- auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), QString::SkipEmptyParts);
-#else
auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), Qt::SkipEmptyParts);
-#endif
if (parts.startsWith(".") && !path.startsWith(".")) {
parts.removeFirst();
@@ -950,7 +922,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
QDir content = application.path() + "/Contents/";
QDir resources = content.path() + "/Resources/";
QDir binaryDir = content.path() + "/MacOS/";
- QFile info = content.path() + "/Info.plist";
+ QFile info(content.path() + "/Info.plist");
if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) {
qWarning() << "Couldn't create directories within application";
diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h
index c5beef7bd..bf91c603c 100644
--- a/launcher/FileSystem.h
+++ b/launcher/FileSystem.h
@@ -115,7 +115,7 @@ class copy : public QObject {
m_followSymlinks = follow;
return *this;
}
- copy& matcher(const IPathMatcher* filter)
+ copy& matcher(IPathMatcher::Ptr filter)
{
m_matcher = filter;
return *this;
@@ -147,7 +147,7 @@ class copy : public QObject {
private:
bool m_followSymlinks = true;
- const IPathMatcher* m_matcher = nullptr;
+ IPathMatcher::Ptr m_matcher = nullptr;
bool m_whitelist = false;
bool m_overwrite = false;
QDir m_src;
@@ -209,7 +209,7 @@ class create_link : public QObject {
m_useHardLinks = useHard;
return *this;
}
- create_link& matcher(const IPathMatcher* filter)
+ create_link& matcher(IPathMatcher::Ptr filter)
{
m_matcher = filter;
return *this;
@@ -260,7 +260,7 @@ class create_link : public QObject {
private:
bool m_useHardLinks = false;
- const IPathMatcher* m_matcher = nullptr;
+ IPathMatcher::Ptr m_matcher = nullptr;
bool m_whitelist = false;
bool m_recursive = true;
@@ -488,7 +488,7 @@ class clone : public QObject {
m_src.setPath(src);
m_dst.setPath(dst);
}
- clone& matcher(const IPathMatcher* filter)
+ clone& matcher(IPathMatcher::Ptr filter)
{
m_matcher = filter;
return *this;
@@ -514,7 +514,7 @@ class clone : public QObject {
bool operator()(const QString& offset, bool dryRun = false);
private:
- const IPathMatcher* m_matcher = nullptr;
+ IPathMatcher::Ptr m_matcher = nullptr;
bool m_whitelist = false;
QDir m_src;
QDir m_dst;
diff --git a/launcher/GZip.cpp b/launcher/GZip.cpp
index 1c2539e08..29c71c012 100644
--- a/launcher/GZip.cpp
+++ b/launcher/GZip.cpp
@@ -36,6 +36,8 @@
#include "GZip.h"
#include
#include
+#include
+#include
bool GZip::unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes)
{
@@ -136,3 +138,81 @@ bool GZip::zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes)
}
return true;
}
+
+int inf(QFile* source, std::function handleBlock)
+{
+ constexpr auto CHUNK = 16384;
+ int ret;
+ unsigned have;
+ z_stream strm;
+ memset(&strm, 0, sizeof(strm));
+ char in[CHUNK];
+ unsigned char out[CHUNK];
+
+ ret = inflateInit2(&strm, (16 + MAX_WBITS));
+ if (ret != Z_OK)
+ return ret;
+
+ /* decompress until deflate stream ends or end of file */
+ do {
+ strm.avail_in = source->read(in, CHUNK);
+ if (source->error()) {
+ (void)inflateEnd(&strm);
+ return Z_ERRNO;
+ }
+ if (strm.avail_in == 0)
+ break;
+ strm.next_in = reinterpret_cast(in);
+
+ /* run inflate() on input until output buffer not full */
+ do {
+ strm.avail_out = CHUNK;
+ strm.next_out = out;
+ ret = inflate(&strm, Z_NO_FLUSH);
+ assert(ret != Z_STREAM_ERROR); /* state not clobbered */
+ switch (ret) {
+ case Z_NEED_DICT:
+ ret = Z_DATA_ERROR; /* and fall through */
+ case Z_DATA_ERROR:
+ case Z_MEM_ERROR:
+ (void)inflateEnd(&strm);
+ return ret;
+ }
+ have = CHUNK - strm.avail_out;
+ if (!handleBlock(QByteArray(reinterpret_cast(out), have))) {
+ (void)inflateEnd(&strm);
+ return Z_OK;
+ }
+
+ } while (strm.avail_out == 0);
+
+ /* done when inflate() says it's done */
+ } while (ret != Z_STREAM_END);
+
+ /* clean up and return */
+ (void)inflateEnd(&strm);
+ return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR;
+}
+
+QString zerr(int ret)
+{
+ switch (ret) {
+ case Z_ERRNO:
+ return QObject::tr("error handling file");
+ case Z_STREAM_ERROR:
+ return QObject::tr("invalid compression level");
+ case Z_DATA_ERROR:
+ return QObject::tr("invalid or incomplete deflate data");
+ case Z_MEM_ERROR:
+ return QObject::tr("out of memory");
+ case Z_VERSION_ERROR:
+ return QObject::tr("zlib version mismatch!");
+ }
+ return {};
+}
+
+QString GZip::readGzFileByBlocks(QFile* source, std::function handleBlock)
+{
+ auto ret = inf(source, handleBlock);
+ return zerr(ret);
+}
\ No newline at end of file
diff --git a/launcher/GZip.h b/launcher/GZip.h
index 0bdb70407..b736ca93f 100644
--- a/launcher/GZip.h
+++ b/launcher/GZip.h
@@ -1,8 +1,11 @@
#pragma once
#include
+#include
-class GZip {
- public:
- static bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes);
- static bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes);
-};
+namespace GZip {
+
+bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes);
+bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes);
+QString readGzFileByBlocks(QFile* source, std::function handleBlock);
+
+} // namespace GZip
diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp
index d335b11c4..fb5963532 100644
--- a/launcher/InstanceCopyTask.cpp
+++ b/launcher/InstanceCopyTask.cpp
@@ -43,7 +43,7 @@ void InstanceCopyTask::executeTask()
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] {
if (m_useClone) {
FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath);
- folderClone.matcher(m_matcher.get());
+ folderClone.matcher(m_matcher);
folderClone(true);
setProgress(0, folderClone.totalCloned());
@@ -72,7 +72,7 @@ void InstanceCopyTask::executeTask()
}
FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath);
int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder
- folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get());
+ folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher);
folderLink(true);
setProgress(0, m_progressTotal + folderLink.totalToLink());
@@ -127,7 +127,7 @@ void InstanceCopyTask::executeTask()
return !there_were_errors;
}
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
- folderCopy.followSymlinks(false).matcher(m_matcher.get());
+ folderCopy.followSymlinks(false).matcher(m_matcher);
folderCopy(true);
setProgress(0, folderCopy.totalCopied());
diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h
index 0f7f1020d..3aba13e5c 100644
--- a/launcher/InstanceCopyTask.h
+++ b/launcher/InstanceCopyTask.h
@@ -28,7 +28,7 @@ class InstanceCopyTask : public InstanceTask {
InstancePtr m_origInstance;
QFuture m_copyFuture;
QFutureWatcher m_copyFutureWatcher;
- std::unique_ptr m_matcher;
+ IPathMatcher::Ptr m_matcher;
bool m_keepPlaytime;
bool m_useLinks = false;
bool m_useHardLinks = false;
diff --git a/launcher/InstanceDirUpdate.cpp b/launcher/InstanceDirUpdate.cpp
new file mode 100644
index 000000000..8be0dccac
--- /dev/null
+++ b/launcher/InstanceDirUpdate.cpp
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "InstanceDirUpdate.h"
+
+#include
+
+#include "Application.h"
+#include "FileSystem.h"
+
+#include "InstanceList.h"
+#include "ui/dialogs/CustomMessageBox.h"
+
+QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent)
+{
+ if (oldName == newName)
+ return QString();
+
+ QString renamingMode = APPLICATION->settings()->get("InstRenamingMode").toString();
+ if (renamingMode == "MetadataOnly")
+ return QString();
+
+ auto oldRoot = instance->instanceRoot();
+ auto newDirName = FS::DirNameFromString(newName, QFileInfo(oldRoot).dir().absolutePath());
+ auto newRoot = FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newDirName);
+ if (oldRoot == newRoot)
+ return QString();
+ if (oldRoot == FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newName))
+ return QString();
+
+ // Check for conflict
+ if (QDir(newRoot).exists()) {
+ QMessageBox::warning(parent, QObject::tr("Cannot rename instance"),
+ QObject::tr("New instance root (%1) already exists.
Only the metadata will be renamed.").arg(newRoot));
+ return QString();
+ }
+
+ // Ask if we should rename
+ if (renamingMode == "AskEverytime") {
+ auto checkBox = new QCheckBox(QObject::tr("&Remember my choice"), parent);
+ auto dialog =
+ CustomMessageBox::selectable(parent, QObject::tr("Rename instance folder"),
+ QObject::tr("Would you also like to rename the instance folder?\n\n"
+ "Old name: %1\n"
+ "New name: %2")
+ .arg(oldName, newName),
+ QMessageBox::Question, QMessageBox::No | QMessageBox::Yes, QMessageBox::NoButton, checkBox);
+
+ auto res = dialog->exec();
+ if (checkBox->isChecked()) {
+ if (res == QMessageBox::Yes)
+ APPLICATION->settings()->set("InstRenamingMode", "PhysicalDir");
+ else
+ APPLICATION->settings()->set("InstRenamingMode", "MetadataOnly");
+ }
+ if (res == QMessageBox::No)
+ return QString();
+ }
+
+ // Check for linked instances
+ if (!checkLinkedInstances(instance->id(), parent, QObject::tr("Renaming")))
+ return QString();
+
+ // Now we can confirm that a renaming is happening
+ if (!instance->syncInstanceDirName(newRoot)) {
+ QMessageBox::warning(parent, QObject::tr("Cannot rename instance"),
+ QObject::tr("An error occurred when performing the following renaming operation:
"
+ " - Old instance root: %1
"
+ " - New instance root: %2
"
+ "Only the metadata is renamed.")
+ .arg(oldRoot, newRoot));
+ return QString();
+ }
+ return newRoot;
+}
+
+bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb)
+{
+ auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id);
+ if (!linkedInstances.empty()) {
+ auto response = CustomMessageBox::selectable(parent, QObject::tr("There are linked instances"),
+ QObject::tr("The following instance(s) might reference files in this instance:\n\n"
+ "%1\n\n"
+ "%2 it could break the other instance(s), \n\n"
+ "Do you wish to proceed?",
+ nullptr, linkedInstances.count())
+ .arg(linkedInstances.join("\n"))
+ .arg(verb),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
+ ->exec();
+ if (response != QMessageBox::Yes)
+ return false;
+ }
+ return true;
+}
diff --git a/launcher/InstanceDirUpdate.h b/launcher/InstanceDirUpdate.h
new file mode 100644
index 000000000..b92a59c4c
--- /dev/null
+++ b/launcher/InstanceDirUpdate.h
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "BaseInstance.h"
+
+/// Update instanceRoot to make it sync with name/id; return newRoot if a directory rename happened
+QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent);
+
+/// Check if there are linked instances, and display a warning; return true if the operation should proceed
+bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb);
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
index 71630656d..633382404 100644
--- a/launcher/InstanceImportTask.cpp
+++ b/launcher/InstanceImportTask.cpp
@@ -378,8 +378,8 @@ void InstanceImportTask::processModrinth()
} else {
QString pack_id;
if (!m_sourceUrl.isEmpty()) {
- QRegularExpression regex(R"(data\/([^\/]*)\/versions)");
- pack_id = regex.match(m_sourceUrl.toString()).captured(1);
+ static const QRegularExpression s_regex(R"(data\/([^\/]*)\/versions)");
+ pack_id = s_regex.match(m_sourceUrl.toString()).captured(1);
}
// FIXME: Find a way to get the ID in directly imported ZIPs
diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp
index 918fa1073..89e7dc04d 100644
--- a/launcher/InstanceList.cpp
+++ b/launcher/InstanceList.cpp
@@ -428,7 +428,7 @@ static QMap getIdMapping(const QList&
QList InstanceList::discoverInstances()
{
- qDebug() << "Discovering instances in" << m_instDir;
+ qInfo() << "Discovering instances in" << m_instDir;
QList out;
QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks);
while (iter.hasNext()) {
@@ -447,13 +447,9 @@ QList InstanceList::discoverInstances()
}
auto id = dirInfo.fileName();
out.append(id);
- qDebug() << "Found instance ID" << id;
+ qInfo() << "Found instance ID" << id;
}
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
instanceSet = QSet(out.begin(), out.end());
-#else
- instanceSet = out.toSet();
-#endif
m_instancesProbed = true;
return out;
}
@@ -468,7 +464,7 @@ InstanceList::InstListError InstanceList::loadList()
if (existingIds.contains(id)) {
auto instPair = existingIds[id];
existingIds.remove(id);
- qDebug() << "Should keep and soft-reload" << id;
+ qInfo() << "Should keep and soft-reload" << id;
} else {
InstancePtr instPtr = loadInstance(id);
if (instPtr) {
diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h
index e4884d11f..2c2b0b580 100644
--- a/launcher/InstancePageProvider.h
+++ b/launcher/InstancePageProvider.h
@@ -46,10 +46,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider {
// values.append(new GameOptionsPage(onesix.get()));
values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots")));
values.append(new InstanceSettingsPage(onesix));
- auto logMatcher = inst->getLogFileMatcher();
- if (logMatcher) {
- values.append(new OtherLogsPage(inst->getLogFileRoot(), logMatcher));
- }
+ values.append(new OtherLogsPage(inst));
return values;
}
diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp
index 188edb943..b71000054 100644
--- a/launcher/JavaCommon.cpp
+++ b/launcher/JavaCommon.cpp
@@ -41,7 +41,9 @@
bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent)
{
- if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(QRegularExpression("-Xm[sx]")) || jvmargs.contains("-XX-MaxHeapSize") ||
+ static const QRegularExpression s_memRegex("-Xm[sx]");
+ static const QRegularExpression s_versionRegex("-version:.*");
+ if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(s_memRegex) || jvmargs.contains("-XX-MaxHeapSize") ||
jvmargs.contains("-XX:InitialHeapSize")) {
auto warnStr = QObject::tr(
"You tried to manually set a JVM memory option (using \"-XX:PermSize\", \"-XX-MaxHeapSize\", \"-XX:InitialHeapSize\", \"-Xmx\" "
@@ -52,7 +54,7 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent)
return false;
}
// block lunacy with passing required version to the JVM
- if (jvmargs.contains(QRegularExpression("-version:.*"))) {
+ if (jvmargs.contains(s_versionRegex)) {
auto warnStr = QObject::tr(
"You tried to pass required Java version argument to the JVM (using \"-version:xxx\"). This is not safe and will not be "
"allowed.\n"
diff --git a/launcher/JavaCommon.h b/launcher/JavaCommon.h
index a21b5a494..0e4aa2b0a 100644
--- a/launcher/JavaCommon.h
+++ b/launcher/JavaCommon.h
@@ -24,7 +24,7 @@ class TestCheck : public QObject {
TestCheck(QWidget* parent, QString path, QString args, int minMem, int maxMem, int permGen)
: m_parent(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen)
{}
- virtual ~TestCheck() {};
+ virtual ~TestCheck() = default;
void run();
diff --git a/launcher/Json.h b/launcher/Json.h
index 28891f398..c13be6470 100644
--- a/launcher/Json.h
+++ b/launcher/Json.h
@@ -188,10 +188,10 @@ T ensureIsType(const QJsonObject& parent, const QString& key, const T default_ =
}
template
-QVector requireIsArrayOf(const QJsonDocument& doc)
+QList requireIsArrayOf(const QJsonDocument& doc)
{
const QJsonArray array = requireArray(doc);
- QVector out;
+ QList out;
for (const QJsonValue val : array) {
out.append(requireIsType(val, "Document"));
}
@@ -199,10 +199,10 @@ QVector requireIsArrayOf(const QJsonDocument& doc)
}
template
-QVector ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value")
+QList ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value")
{
const QJsonArray array = ensureIsType(value, QJsonArray(), what);
- QVector out;
+ QList out;
for (const QJsonValue val : array) {
out.append(requireIsType(val, what));
}
@@ -210,7 +210,7 @@ QVector ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value
}
template
-QVector ensureIsArrayOf(const QJsonValue& value, const QVector default_, const QString& what = "Value")
+QList ensureIsArrayOf(const QJsonValue& value, const QList default_, const QString& what = "Value")
{
if (value.isUndefined()) {
return default_;
@@ -220,7 +220,7 @@ QVector ensureIsArrayOf(const QJsonValue& value, const QVector default_, c
/// @throw JsonException
template
-QVector requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__")
+QList requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__")
{
const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
if (!parent.contains(key)) {
@@ -230,10 +230,10 @@ QVector requireIsArrayOf(const QJsonObject& parent, const QString& key, const
}
template
-QVector ensureIsArrayOf(const QJsonObject& parent,
- const QString& key,
- const QVector& default_ = QVector(),
- const QString& what = "__placeholder__")
+QList ensureIsArrayOf(const QJsonObject& parent,
+ const QString& key,
+ const QList& default_ = QList(),
+ const QString& what = "__placeholder__")
{
const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
if (!parent.contains(key)) {
diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp
index 07047bf67..b1a956b49 100644
--- a/launcher/LaunchController.cpp
+++ b/launcher/LaunchController.cpp
@@ -182,7 +182,8 @@ void LaunchController::login()
auto name = askOfflineName("Player", m_demo, ok);
if (ok) {
m_session = std::make_shared();
- m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(QRegularExpression("[{}-]")));
+ static const QRegularExpression s_removeChars("[{}-]");
+ m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(s_removeChars));
launchInstance();
return;
}
diff --git a/launcher/MessageLevel.cpp b/launcher/MessageLevel.cpp
index 116e70c4b..2bd6ecc00 100644
--- a/launcher/MessageLevel.cpp
+++ b/launcher/MessageLevel.cpp
@@ -2,19 +2,22 @@
MessageLevel::Enum MessageLevel::getLevel(const QString& levelName)
{
- if (levelName == "Launcher")
+ QString name = levelName.toUpper();
+ if (name == "LAUNCHER")
return MessageLevel::Launcher;
- else if (levelName == "Debug")
+ else if (name == "TRACE")
+ return MessageLevel::Trace;
+ else if (name == "DEBUG")
return MessageLevel::Debug;
- else if (levelName == "Info")
+ else if (name == "INFO")
return MessageLevel::Info;
- else if (levelName == "Message")
+ else if (name == "MESSAGE")
return MessageLevel::Message;
- else if (levelName == "Warning")
+ else if (name == "WARNING" || name == "WARN")
return MessageLevel::Warning;
- else if (levelName == "Error")
+ else if (name == "ERROR")
return MessageLevel::Error;
- else if (levelName == "Fatal")
+ else if (name == "FATAL")
return MessageLevel::Fatal;
// Skip PrePost, it's not exposed to !![]!
// Also skip StdErr and StdOut
diff --git a/launcher/MessageLevel.h b/launcher/MessageLevel.h
index fd12583f2..321af9d92 100644
--- a/launcher/MessageLevel.h
+++ b/launcher/MessageLevel.h
@@ -12,6 +12,7 @@ enum Enum {
StdOut, /**< Undetermined stderr messages */
StdErr, /**< Undetermined stdout messages */
Launcher, /**< Launcher Messages */
+ Trace, /**< Trace Messages */
Debug, /**< Debug Messages */
Info, /**< Info Messages */
Message, /**< Standard Messages */
diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h
index 3d01c9d33..e603b1634 100644
--- a/launcher/NullInstance.h
+++ b/launcher/NullInstance.h
@@ -57,8 +57,7 @@ class NullInstance : public BaseInstance {
QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); }
QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); }
QMap getVariables() override { return QMap(); }
- IPathMatcher::Ptr getLogFileMatcher() override { return nullptr; }
- QString getLogFileRoot() override { return instanceRoot(); }
+ QStringList getLogFileSearchPaths() override { return {}; }
QString typeName() const override { return "Null"; }
bool canExport() const override { return false; }
bool canEdit() const override { return false; }
diff --git a/launcher/RecursiveFileSystemWatcher.cpp b/launcher/RecursiveFileSystemWatcher.cpp
index 8b28a03f1..5cb3cd0be 100644
--- a/launcher/RecursiveFileSystemWatcher.cpp
+++ b/launcher/RecursiveFileSystemWatcher.cpp
@@ -1,7 +1,6 @@
#include "RecursiveFileSystemWatcher.h"
#include
-#include
RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject* parent) : QObject(parent), m_watcher(new QFileSystemWatcher(this))
{
diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp
index edda9f247..b9e875482 100644
--- a/launcher/StringUtils.cpp
+++ b/launcher/StringUtils.cpp
@@ -53,7 +53,7 @@ static inline QChar getNextChar(const QString& s, int location)
int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs)
{
int l1 = 0, l2 = 0;
- while (l1 <= s1.count() && l2 <= s2.count()) {
+ while (l1 <= s1.size() && l2 <= s2.size()) {
// skip spaces, tabs and 0's
QChar c1 = getNextChar(s1, l1);
while (c1.isSpace())
@@ -213,11 +213,10 @@ QPair StringUtils::splitFirst(const QString& s, const QRegular
return qMakePair(left, right);
}
-static const QRegularExpression ulMatcher("<\\s*/\\s*ul\\s*>");
-
QString StringUtils::htmlListPatch(QString htmlStr)
{
- int pos = htmlStr.indexOf(ulMatcher);
+ static const QRegularExpression s_ulMatcher("<\\s*/\\s*ul\\s*>");
+ int pos = htmlStr.indexOf(s_ulMatcher);
int imgPos;
while (pos != -1) {
pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the tag. Add one for zeroeth index
@@ -230,7 +229,7 @@ QString StringUtils::htmlListPatch(QString htmlStr)
if (textBetween.isEmpty())
htmlStr.insert(pos, "
");
- pos = htmlStr.indexOf(ulMatcher, pos);
+ pos = htmlStr.indexOf(s_ulMatcher, pos);
}
return htmlStr;
}
\ No newline at end of file
diff --git a/launcher/Version.cpp b/launcher/Version.cpp
index 03a16e8a0..bffe5d58a 100644
--- a/launcher/Version.cpp
+++ b/launcher/Version.cpp
@@ -1,7 +1,6 @@
#include "Version.h"
#include
-#include
#include
#include
diff --git a/launcher/Version.h b/launcher/Version.h
index b06e256aa..12e7f0832 100644
--- a/launcher/Version.h
+++ b/launcher/Version.h
@@ -72,22 +72,14 @@ class Version {
}
}
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
auto numPart = QStringView{ m_fullString }.left(cutoff);
-#else
- auto numPart = m_fullString.leftRef(cutoff);
-#endif
if (!numPart.isEmpty()) {
m_isNull = false;
m_numPart = numPart.toInt();
}
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
auto stringPart = QStringView{ m_fullString }.mid(cutoff);
-#else
- auto stringPart = m_fullString.midRef(cutoff);
-#endif
if (!stringPart.isEmpty()) {
m_isNull = false;
diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp
index 7538ce08c..165dd4cb7 100644
--- a/launcher/VersionProxyModel.cpp
+++ b/launcher/VersionProxyModel.cpp
@@ -193,8 +193,8 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const
if (value.toBool()) {
return tr("Recommended");
} else if (hasLatest) {
- auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
- if (value.toBool()) {
+ auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
+ if (latest.toBool()) {
return tr("Latest");
}
}
@@ -203,33 +203,27 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const
}
}
case Qt::DecorationRole: {
- switch (column) {
- case Name: {
- if (hasRecommended) {
- auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
- if (recommenced.toBool()) {
- return APPLICATION->getThemedIcon("star");
- } else if (hasLatest) {
- auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
- if (latest.toBool()) {
- return APPLICATION->getThemedIcon("bug");
- }
- }
- QPixmap pixmap;
- QPixmapCache::find("placeholder", &pixmap);
- if (!pixmap) {
- QPixmap px(16, 16);
- px.fill(Qt::transparent);
- QPixmapCache::insert("placeholder", px);
- return px;
- }
- return pixmap;
+ if (column == Name && hasRecommended) {
+ auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
+ if (recommenced.toBool()) {
+ return APPLICATION->getThemedIcon("star");
+ } else if (hasLatest) {
+ auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
+ if (latest.toBool()) {
+ return APPLICATION->getThemedIcon("bug");
}
}
- default: {
- return QVariant();
+ QPixmap pixmap;
+ QPixmapCache::find("placeholder", &pixmap);
+ if (!pixmap) {
+ QPixmap px(16, 16);
+ px.fill(Qt::transparent);
+ QPixmapCache::insert("placeholder", px);
+ return px;
}
+ return pixmap;
}
+ return QVariant();
}
default: {
if (roles.contains((BaseVersionList::ModelRoles)role)) {
@@ -301,13 +295,11 @@ void VersionProxyModel::sourceDataChanged(const QModelIndex& source_top_left, co
void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw)
{
auto replacing = dynamic_cast(replacingRaw);
- beginResetModel();
m_columns.clear();
if (!replacing) {
roles.clear();
filterModel->setSourceModel(replacing);
- endResetModel();
return;
}
@@ -349,8 +341,6 @@ void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw)
hasLatest = true;
}
filterModel->setSourceModel(replacing);
-
- endResetModel();
}
QModelIndex VersionProxyModel::getRecommended() const
diff --git a/launcher/console/Console.h b/launcher/console/Console.h
new file mode 100644
index 000000000..7aaf83dcc
--- /dev/null
+++ b/launcher/console/Console.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include
+
+#include
+#if defined Q_OS_WIN32
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#include
+#else
+#include
+#include
+#endif
+
+namespace console {
+
+inline bool isConsole()
+{
+#if defined Q_OS_WIN32
+ DWORD procIDs[2];
+ DWORD maxCount = 2;
+ DWORD result = GetConsoleProcessList((LPDWORD)procIDs, maxCount);
+ return result > 1;
+#else
+ if (isatty(fileno(stdout))) {
+ return true;
+ }
+ return false;
+#endif
+}
+
+} // namespace console
diff --git a/launcher/WindowsConsole.cpp b/launcher/console/WindowsConsole.cpp
similarity index 78%
rename from launcher/WindowsConsole.cpp
rename to launcher/console/WindowsConsole.cpp
index 83cad5afa..4a0eb3d3d 100644
--- a/launcher/WindowsConsole.cpp
+++ b/launcher/console/WindowsConsole.cpp
@@ -16,13 +16,18 @@
*
*/
+#include "WindowsConsole.h"
+#include
+
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
+#include
+
#include
#include
#include
-#include
+#include
#include
void RedirectHandle(DWORD handle, FILE* stream, const char* mode)
@@ -126,3 +131,29 @@ bool AttachWindowsConsole()
return false;
}
+
+std::error_code EnableAnsiSupport()
+{
+ // ref: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew
+ // Using `CreateFileW("CONOUT$", ...)` to retrieve the console handle works correctly even if STDOUT and/or STDERR are redirected
+ HANDLE console_handle = CreateFileW(L"CONOUT$", FILE_GENERIC_READ | FILE_GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
+ if (console_handle == INVALID_HANDLE_VALUE) {
+ return std::error_code(GetLastError(), std::system_category());
+ }
+
+ // ref: https://docs.microsoft.com/en-us/windows/console/getconsolemode
+ DWORD console_mode;
+ if (0 == GetConsoleMode(console_handle, &console_mode)) {
+ return std::error_code(GetLastError(), std::system_category());
+ }
+
+ // VT processing not already enabled?
+ if ((console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0) {
+ // https://docs.microsoft.com/en-us/windows/console/setconsolemode
+ if (0 == SetConsoleMode(console_handle, console_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) {
+ return std::error_code(GetLastError(), std::system_category());
+ }
+ }
+
+ return {};
+}
diff --git a/launcher/WindowsConsole.h b/launcher/console/WindowsConsole.h
similarity index 93%
rename from launcher/WindowsConsole.h
rename to launcher/console/WindowsConsole.h
index ab53864b4..4c1f3ee28 100644
--- a/launcher/WindowsConsole.h
+++ b/launcher/console/WindowsConsole.h
@@ -21,5 +21,8 @@
#pragma once
+#include
+
void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr);
bool AttachWindowsConsole();
+std::error_code EnableAnsiSupport();
diff --git a/launcher/filelink/FileLink.cpp b/launcher/filelink/FileLink.cpp
index b641b41d5..1494fa8cc 100644
--- a/launcher/filelink/FileLink.cpp
+++ b/launcher/filelink/FileLink.cpp
@@ -37,27 +37,14 @@
#include
#if defined Q_OS_WIN32
-#include "WindowsConsole.h"
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#include "console/WindowsConsole.h"
#endif
-// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
-
-#ifdef __APPLE__
-#include // for deployment target to support pre-catalina targets without std::fs
-#endif // __APPLE__
-
-#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
-#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
-#define GHC_USE_STD_FS
#include
namespace fs = std::filesystem;
-#endif // MacOS min version check
-#endif // Other OSes version check
-
-#ifndef GHC_USE_STD_FS
-#include
-namespace fs = ghc::filesystem;
-#endif
FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this))
{
diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp
index 8324663a1..8a2a482e1 100644
--- a/launcher/icons/IconList.cpp
+++ b/launcher/icons/IconList.cpp
@@ -137,11 +137,7 @@ QString formatName(const QDir& iconsDir, const QFileInfo& iconFile)
/// Split into a separate function because the preprocessing impedes readability
QSet toStringSet(const QList& list)
{
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QSet set(list.begin(), list.end());
-#else
- QSet set = list.toSet();
-#endif
return set;
}
@@ -165,7 +161,8 @@ void IconList::directoryChanged(const QString& path)
for (const MMCIcon& it : m_icons) {
if (!it.has(IconType::FileBased))
continue;
- currentSet.insert(it.m_images[IconType::FileBased].filename);
+ QFileInfo icon(it.getFilePath());
+ currentSet.insert(icon.absoluteFilePath());
}
QSet toRemove = currentSet - newSet;
QSet toAdd = newSet - currentSet;
@@ -173,7 +170,8 @@ void IconList::directoryChanged(const QString& path)
for (const QString& removedPath : toRemove) {
qDebug() << "Removing icon " << 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);
if (idx == -1)
@@ -475,4 +473,4 @@ QString IconList::iconDirectory(const QString& key) const
}
}
return getDirectory();
-}
+}
\ No newline at end of file
diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h
index 8936195c3..d2f904448 100644
--- a/launcher/icons/IconList.h
+++ b/launcher/icons/IconList.h
@@ -106,6 +106,6 @@ class IconList : public QAbstractListModel {
shared_qobject_ptr m_watcher;
bool m_isWatching;
QMap m_nameIndex;
- QVector m_icons;
+ QList m_icons;
QDir m_dir;
};
diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp
index 07b5d7b40..0aa725705 100644
--- a/launcher/java/JavaChecker.cpp
+++ b/launcher/java/JavaChecker.cpp
@@ -137,11 +137,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status)
QMap results;
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QStringList lines = m_stdout.split("\n", Qt::SkipEmptyParts);
-#else
- QStringList lines = m_stdout.split("\n", QString::SkipEmptyParts);
-#endif
for (QString line : lines) {
line = line.trimmed();
// NOTE: workaround for GH-4125, where garbage is getting printed into stdout on bedrock linux
@@ -149,11 +145,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status)
continue;
}
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
auto parts = line.split('=', Qt::SkipEmptyParts);
-#else
- auto parts = line.split('=', QString::SkipEmptyParts);
-#endif
if (parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) {
continue;
} else {
diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp
index bca50f2c9..e9a160ea7 100644
--- a/launcher/java/JavaVersion.cpp
+++ b/launcher/java/JavaVersion.cpp
@@ -19,9 +19,13 @@ JavaVersion& JavaVersion::operator=(const QString& javaVersionString)
QRegularExpression pattern;
if (javaVersionString.startsWith("1.")) {
- pattern = QRegularExpression("1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?");
+ static const QRegularExpression s_withOne(
+ "1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?");
+ pattern = s_withOne;
} else {
- pattern = QRegularExpression("(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?");
+ static const QRegularExpression s_withoutOne(
+ "(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?");
+ pattern = s_withoutOne;
}
auto match = pattern.match(m_string);
diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp
index 9ec746641..b67df7631 100644
--- a/launcher/launch/LaunchTask.cpp
+++ b/launcher/launch/LaunchTask.cpp
@@ -37,11 +37,12 @@
#include "launch/LaunchTask.h"
#include
+#include
#include
#include
#include
-#include
#include
+#include
#include "MessageLevel.h"
#include "tasks/Task.h"
@@ -213,6 +214,52 @@ shared_qobject_ptr LaunchTask::getLogModel()
return m_logModel;
}
+bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel::Enum level)
+{
+ LogParser* parser;
+ switch (level) {
+ case MessageLevel::StdErr:
+ parser = &m_stderrParser;
+ break;
+ case MessageLevel::StdOut:
+ parser = &m_stdoutParser;
+ break;
+ default:
+ return false;
+ }
+
+ parser->appendLine(line);
+ auto items = parser->parseAvailable();
+ if (auto err = parser->getError(); err.has_value()) {
+ auto& model = *getLogModel();
+ model.append(MessageLevel::Error, tr("[Log4j Parse Error] Failed to parse log4j log event: %1").arg(err.value().errMessage));
+ return false;
+ } else {
+ if (!items.isEmpty()) {
+ auto& model = *getLogModel();
+ for (auto const& item : items) {
+ if (std::holds_alternative(item)) {
+ auto entry = std::get(item);
+ auto msg = QString("[%1] [%2/%3] [%4]: %5")
+ .arg(entry.timestamp.toString("HH:mm:ss"))
+ .arg(entry.thread)
+ .arg(entry.levelText)
+ .arg(entry.logger)
+ .arg(entry.message);
+ msg = censorPrivateInfo(msg);
+ model.append(entry.level, msg);
+ } else if (std::holds_alternative(item)) {
+ auto msg = std::get(item).message;
+ level = LogParser::guessLevel(msg, model.previousLevel());
+ msg = censorPrivateInfo(msg);
+ model.append(level, msg);
+ }
+ }
+ }
+ }
+ return true;
+}
+
void LaunchTask::onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel)
{
for (auto& line : lines) {
@@ -222,21 +269,26 @@ void LaunchTask::onLogLines(const QStringList& lines, MessageLevel::Enum default
void LaunchTask::onLogLine(QString line, MessageLevel::Enum level)
{
+ if (parseXmlLogs(line, level)) {
+ return;
+ }
+
// if the launcher part set a log level, use it
auto innerLevel = MessageLevel::fromLine(line);
if (innerLevel != MessageLevel::Unknown) {
level = innerLevel;
}
+ auto& model = *getLogModel();
+
// If the level is still undetermined, guess level
- if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) {
- level = m_instance->guessLevel(line, level);
+ if (level == MessageLevel::Unknown) {
+ level = LogParser::guessLevel(line, model.previousLevel());
}
// censor private user info
line = censorPrivateInfo(line);
- auto& model = *getLogModel();
model.append(level, line);
}
diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h
index 2e87ece95..5effab980 100644
--- a/launcher/launch/LaunchTask.h
+++ b/launcher/launch/LaunchTask.h
@@ -43,6 +43,7 @@
#include "LaunchStep.h"
#include "LogModel.h"
#include "MessageLevel.h"
+#include "logs/LogParser.h"
class LaunchTask : public Task {
Q_OBJECT
@@ -114,6 +115,9 @@ class LaunchTask : public Task {
private: /*methods */
void finalizeSteps(bool successful, const QString& error);
+ protected:
+ bool parseXmlLogs(QString const& line, MessageLevel::Enum level);
+
protected: /* data */
MinecraftInstancePtr m_instance;
shared_qobject_ptr m_logModel;
@@ -122,4 +126,6 @@ class LaunchTask : public Task {
int currentStep = -1;
State state = NotStarted;
qint64 m_pid = -1;
+ LogParser m_stdoutParser;
+ LogParser m_stderrParser;
};
diff --git a/launcher/launch/LogModel.cpp b/launcher/launch/LogModel.cpp
index 23a33ae18..53a450ff7 100644
--- a/launcher/launch/LogModel.cpp
+++ b/launcher/launch/LogModel.cpp
@@ -100,7 +100,7 @@ void LogModel::setMaxLines(int maxLines)
return;
}
// otherwise, we need to reorganize the data because it crosses the wrap boundary
- QVector newContent;
+ QList newContent;
newContent.resize(maxLines);
if (m_numLines <= maxLines) {
// if it all fits in the new buffer, just copy it over
@@ -149,3 +149,28 @@ bool LogModel::wrapLines() const
{
return m_lineWrap;
}
+
+void LogModel::setColorLines(bool state)
+{
+ if (m_colorLines != state) {
+ m_colorLines = state;
+ }
+}
+
+bool LogModel::colorLines() const
+{
+ return m_colorLines;
+}
+
+bool LogModel::isOverFlow()
+{
+ return m_numLines >= m_maxLines && m_stopOnOverflow;
+}
+
+
+MessageLevel::Enum LogModel::previousLevel() {
+ if (!m_content.isEmpty()) {
+ return m_content.last().level;
+ }
+ return MessageLevel::Unknown;
+}
diff --git a/launcher/launch/LogModel.h b/launcher/launch/LogModel.h
index 167f74190..4521bac17 100644
--- a/launcher/launch/LogModel.h
+++ b/launcher/launch/LogModel.h
@@ -24,9 +24,14 @@ class LogModel : public QAbstractListModel {
void setMaxLines(int maxLines);
void setStopOnOverflow(bool stop);
void setOverflowMessage(const QString& overflowMessage);
+ bool isOverFlow();
void setLineWrap(bool state);
bool wrapLines() const;
+ void setColorLines(bool state);
+ bool colorLines() const;
+
+ MessageLevel::Enum previousLevel();
enum Roles { LevelRole = Qt::UserRole };
@@ -37,7 +42,7 @@ class LogModel : public QAbstractListModel {
};
private: /* data */
- QVector m_content;
+ QList m_content;
int m_maxLines = 1000;
// first line in the circular buffer
int m_firstLine = 0;
@@ -47,6 +52,7 @@ class LogModel : public QAbstractListModel {
QString m_overflowMessage = "OVERFLOW";
bool m_suspended = false;
bool m_lineWrap = true;
+ bool m_colorLines = true;
private:
Q_DISABLE_COPY(LogModel)
diff --git a/launcher/launch/steps/PostLaunchCommand.cpp b/launcher/launch/steps/PostLaunchCommand.cpp
index 5d893c71f..6b960974e 100644
--- a/launcher/launch/steps/PostLaunchCommand.cpp
+++ b/launcher/launch/steps/PostLaunchCommand.cpp
@@ -49,14 +49,10 @@ void PostLaunchCommand::executeTask()
{
auto cmd = m_parent->substituteVariables(m_command);
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);
const QString program = args.takeFirst();
m_process.start(program, args);
-#else
- m_process.start(cmd);
-#endif
}
void PostLaunchCommand::on_state(LoggedProcess::State state)
diff --git a/launcher/launch/steps/PreLaunchCommand.cpp b/launcher/launch/steps/PreLaunchCommand.cpp
index 318237e99..7e843ca3f 100644
--- a/launcher/launch/steps/PreLaunchCommand.cpp
+++ b/launcher/launch/steps/PreLaunchCommand.cpp
@@ -49,13 +49,9 @@ void PreLaunchCommand::executeTask()
{
auto cmd = m_parent->substituteVariables(m_command);
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);
const QString program = args.takeFirst();
m_process.start(program, args);
-#else
- m_process.start(cmd);
-#endif
}
void PreLaunchCommand::on_state(LoggedProcess::State state)
diff --git a/launcher/logs/LogParser.cpp b/launcher/logs/LogParser.cpp
new file mode 100644
index 000000000..6e33b24dd
--- /dev/null
+++ b/launcher/logs/LogParser.cpp
@@ -0,0 +1,351 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#include "LogParser.h"
+
+#include
+#include "MessageLevel.h"
+
+using namespace Qt::Literals::StringLiterals;
+
+void LogParser::appendLine(QAnyStringView data)
+{
+ if (!m_partialData.isEmpty()) {
+ m_buffer = QString(m_partialData);
+ m_buffer.append("\n");
+ m_partialData.clear();
+ }
+ m_buffer.append(data.toString());
+}
+
+std::optional LogParser::getError()
+{
+ return m_error;
+}
+
+std::optional LogParser::parseAttributes()
+{
+ LogParser::LogEntry entry{
+ "",
+ MessageLevel::Info,
+ };
+ auto attributes = m_parser.attributes();
+
+ for (const auto& attr : attributes) {
+ auto name = attr.name();
+ auto value = attr.value();
+ if (name == "logger"_L1) {
+ entry.logger = value.trimmed().toString();
+ } else if (name == "timestamp"_L1) {
+ if (value.trimmed().isEmpty()) {
+ m_parser.raiseError("log4j:Event Missing required attribute: timestamp");
+ return {};
+ }
+ entry.timestamp = QDateTime::fromSecsSinceEpoch(value.trimmed().toLongLong());
+ } else if (name == "level"_L1) {
+ entry.levelText = value.trimmed().toString();
+ entry.level = MessageLevel::getLevel(entry.levelText);
+ } else if (name == "thread"_L1) {
+ entry.thread = value.trimmed().toString();
+ }
+ }
+ if (entry.logger.isEmpty()) {
+ m_parser.raiseError("log4j:Event Missing required attribute: logger");
+ return {};
+ }
+
+ return entry;
+}
+
+void LogParser::setError()
+{
+ m_error = {
+ m_parser.errorString(),
+ m_parser.error(),
+ };
+}
+
+void LogParser::clearError()
+{
+ m_error = {}; // clear previous error
+}
+
+bool isPotentialLog4JStart(QStringView buffer)
+{
+ static QString target = QStringLiteral(" LogParser::parseNext()
+{
+ clearError();
+
+ if (m_buffer.isEmpty()) {
+ return {};
+ }
+
+ if (m_buffer.trimmed().isEmpty()) {
+ auto text = QString(m_buffer);
+ m_buffer.clear();
+ return LogParser::PlainText { text };
+ }
+
+ // check if we have a full xml log4j event
+ bool isCompleteLog4j = false;
+ m_parser.clear();
+ m_parser.setNamespaceProcessing(false);
+ m_parser.addData(m_buffer);
+ if (m_parser.readNextStartElement()) {
+ if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) {
+ int depth = 1;
+ bool eod = false;
+ while (depth > 0 && !eod) {
+ auto tok = m_parser.readNext();
+ switch (tok) {
+ case QXmlStreamReader::TokenType::StartElement: {
+ depth += 1;
+ } break;
+ case QXmlStreamReader::TokenType::EndElement: {
+ depth -= 1;
+ } break;
+ case QXmlStreamReader::TokenType::EndDocument: {
+ eod = true; // break outer while loop
+ } break;
+ default: {
+ // no op
+ }
+ }
+ if (m_parser.hasError()) {
+ break;
+ }
+ }
+
+ isCompleteLog4j = depth == 0;
+ }
+ }
+
+ if (isCompleteLog4j) {
+ return parseLog4J();
+ } else {
+ if (isPotentialLog4JStart(m_buffer)) {
+ m_partialData = QString(m_buffer);
+ return LogParser::Partial{ QString(m_buffer) };
+ }
+
+ int start = 0;
+ auto bufView = QStringView(m_buffer);
+ while (start < bufView.length()) {
+ if (qsizetype pos = bufView.right(bufView.length() - start).indexOf('<'); pos != -1) {
+ auto slicestart = start + pos;
+ auto slice = bufView.right(bufView.length() - slicestart);
+ if (isPotentialLog4JStart(slice)) {
+ if (slicestart > 0) {
+ auto text = m_buffer.left(slicestart);
+ m_buffer = m_buffer.right(m_buffer.length() - slicestart);
+ if (!text.trimmed().isEmpty()) {
+ return LogParser::PlainText{ text };
+ }
+ }
+ m_partialData = QString(m_buffer);
+ return LogParser::Partial{ QString(m_buffer) };
+ }
+ start = slicestart + 1;
+ } else {
+ break;
+ }
+ }
+
+ // no log4j found, all plain text
+ auto text = QString(m_buffer);
+ m_buffer.clear();
+ return LogParser::PlainText{ text };
+ }
+}
+
+QList LogParser::parseAvailable()
+{
+ QList items;
+ bool doNext = true;
+ while (doNext) {
+ auto item_ = parseNext();
+ if (m_error.has_value()) {
+ return {};
+ }
+ if (item_.has_value()) {
+ auto item = item_.value();
+ if (std::holds_alternative(item)) {
+ break;
+ } else {
+ items.push_back(item);
+ }
+ } else {
+ doNext = false;
+ }
+ }
+ return items;
+}
+
+std::optional LogParser::parseLog4J()
+{
+ m_parser.clear();
+ m_parser.setNamespaceProcessing(false);
+ m_parser.addData(m_buffer);
+
+ m_parser.readNextStartElement();
+ if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) {
+ auto entry_ = parseAttributes();
+ if (!entry_.has_value()) {
+ setError();
+ return {};
+ }
+ auto entry = entry_.value();
+
+ bool foundMessage = false;
+ int depth = 1;
+
+ enum parseOp { noOp, entryReady, parseError };
+
+ auto foundStart = [&]() -> parseOp {
+ depth += 1;
+ if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) {
+ QString message;
+ bool messageComplete = false;
+
+ while (!messageComplete) {
+ auto tok = m_parser.readNext();
+
+ switch (tok) {
+ case QXmlStreamReader::TokenType::Characters: {
+ message.append(m_parser.text());
+ } break;
+ case QXmlStreamReader::TokenType::EndElement: {
+ if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) {
+ messageComplete = true;
+ }
+ } break;
+ case QXmlStreamReader::TokenType::EndDocument: {
+ return parseError; // parse fail
+ } break;
+ default: {
+ // no op
+ }
+ }
+
+ if (m_parser.hasError()) {
+ return parseError;
+ }
+ }
+
+ entry.message = message;
+ foundMessage = true;
+ depth -= 1;
+ }
+ return noOp;
+ };
+
+ auto foundEnd = [&]() -> parseOp {
+ depth -= 1;
+ if (depth == 0 && m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) {
+ if (foundMessage) {
+ auto consumed = m_parser.characterOffset();
+ if (consumed > 0 && consumed <= m_buffer.length()) {
+ m_buffer = m_buffer.right(m_buffer.length() - consumed);
+ // potential whitespace preserved for next item
+ }
+ clearError();
+ return entryReady;
+ }
+ m_parser.raiseError("log4j:Event Missing required attribute: message");
+ setError();
+ return parseError;
+ }
+ return noOp;
+ };
+
+ while (!m_parser.atEnd()) {
+ auto tok = m_parser.readNext();
+ parseOp op = noOp;
+ switch (tok) {
+ case QXmlStreamReader::TokenType::StartElement: {
+ op = foundStart();
+ } break;
+ case QXmlStreamReader::TokenType::EndElement: {
+ op = foundEnd();
+ } break;
+ case QXmlStreamReader::TokenType::EndDocument: {
+ return {};
+ } break;
+ default: {
+ // no op
+ }
+ }
+
+ switch (op) {
+ case parseError:
+ return {}; // parse fail or error
+ case entryReady:
+ return entry;
+ case noOp:
+ default: {
+ // no op
+ }
+ }
+
+ if (m_parser.hasError()) {
+ return {};
+ }
+ }
+ }
+
+ throw std::runtime_error("unreachable: already verified this was a complete log4j:Event");
+}
+
+MessageLevel::Enum LogParser::guessLevel(const QString& line, MessageLevel::Enum level)
+{
+ static const QRegularExpression LINE_WITH_LEVEL("^\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]");
+ auto match = LINE_WITH_LEVEL.match(line);
+ if (match.hasMatch()) {
+ // New style logs from log4j
+ QString timestamp = match.captured("timestamp");
+ QString levelStr = match.captured("level");
+ level = MessageLevel::getLevel(levelStr);
+ } else {
+ // Old style forge logs
+ if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") ||
+ line.contains("[FINEST]"))
+ level = MessageLevel::Info;
+ if (line.contains("[SEVERE]") || line.contains("[STDERR]"))
+ level = MessageLevel::Error;
+ if (line.contains("[WARNING]"))
+ level = MessageLevel::Warning;
+ if (line.contains("[DEBUG]"))
+ level = MessageLevel::Debug;
+ }
+ if (level != MessageLevel::Unknown)
+ return level;
+
+ if (line.contains("overwriting existing"))
+ return MessageLevel::Fatal;
+
+ return MessageLevel::Info;
+}
diff --git a/launcher/logs/LogParser.h b/launcher/logs/LogParser.h
new file mode 100644
index 000000000..1a1d86dd1
--- /dev/null
+++ b/launcher/logs/LogParser.h
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "MessageLevel.h"
+
+class LogParser {
+ public:
+ struct LogEntry {
+ QString logger;
+ MessageLevel::Enum level;
+ QString levelText;
+ QDateTime timestamp;
+ QString thread;
+ QString message;
+ };
+ struct Partial {
+ QString data;
+ };
+ struct PlainText {
+ QString message;
+ };
+ struct Error {
+ QString errMessage;
+ QXmlStreamReader::Error error;
+ };
+
+ using ParsedItem = std::variant;
+
+ public:
+ LogParser() = default;
+
+ void appendLine(QAnyStringView data);
+ std::optional parseNext();
+ QList parseAvailable();
+ std::optional getError();
+
+ /// guess log level from a line of game log
+ static MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level);
+
+ protected:
+ std::optional parseAttributes();
+ void setError();
+ void clearError();
+
+ std::optional parseLog4J();
+
+ private:
+ QString m_buffer;
+ QString m_partialData;
+ QXmlStreamReader m_parser;
+ std::optional m_error;
+};
diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp
index 1707854be..25a4cd146 100644
--- a/launcher/meta/Index.cpp
+++ b/launcher/meta/Index.cpp
@@ -23,7 +23,7 @@
namespace Meta {
Index::Index(QObject* parent) : QAbstractListModel(parent) {}
-Index::Index(const QVector& lists, QObject* parent) : QAbstractListModel(parent), m_lists(lists)
+Index::Index(const QList& lists, QObject* parent) : QAbstractListModel(parent), m_lists(lists)
{
for (int i = 0; i < m_lists.size(); ++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& other)
{
- const QVector lists = other->m_lists;
+ const QList lists = other->m_lists;
// initial load, no need to merge
if (m_lists.isEmpty()) {
beginResetModel();
diff --git a/launcher/meta/Index.h b/launcher/meta/Index.h
index 026a00c07..fe5bf2170 100644
--- a/launcher/meta/Index.h
+++ b/launcher/meta/Index.h
@@ -29,7 +29,7 @@ class Index : public QAbstractListModel, public BaseEntity {
Q_OBJECT
public:
explicit Index(QObject* parent = nullptr);
- explicit Index(const QVector& lists, QObject* parent = nullptr);
+ explicit Index(const QList& lists, QObject* parent = nullptr);
virtual ~Index() = default;
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);
bool hasUid(const QString& uid) const;
- QVector lists() const { return m_lists; }
+ QList lists() const { return m_lists; }
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;
private:
- QVector m_lists;
+ QList m_lists;
QHash m_uids;
void connectVersionList(int row, const VersionList::Ptr& list);
diff --git a/launcher/meta/JsonFormat.cpp b/launcher/meta/JsonFormat.cpp
index 86af7277e..8d8466c87 100644
--- a/launcher/meta/JsonFormat.cpp
+++ b/launcher/meta/JsonFormat.cpp
@@ -35,8 +35,8 @@ MetadataVersion currentFormatVersion()
// Index
static std::shared_ptr parseIndexInternal(const QJsonObject& obj)
{
- const QVector objects = requireIsArrayOf(obj, "packages");
- QVector lists;
+ const QList objects = requireIsArrayOf(obj, "packages");
+ QList lists;
lists.reserve(objects.size());
std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) {
VersionList::Ptr list = std::make_shared(requireString(obj, "uid"));
@@ -79,8 +79,8 @@ static VersionList::Ptr parseVersionListInternal(const QJsonObject& obj)
{
const QString uid = requireString(obj, "uid");
- const QVector versionsRaw = requireIsArrayOf(obj, "versions");
- QVector versions;
+ const QList versionsRaw = requireIsArrayOf(obj, "versions");
+ QList versions;
versions.reserve(versionsRaw.size());
std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [uid](const QJsonObject& vObj) {
auto version = parseCommonVersion(uid, vObj);
diff --git a/launcher/meta/Version.h b/launcher/meta/Version.h
index 46dc740da..2327879a1 100644
--- a/launcher/meta/Version.h
+++ b/launcher/meta/Version.h
@@ -19,8 +19,8 @@
#include "BaseVersion.h"
#include
+#include
#include
-#include
#include
#include "minecraft/VersionFile.h"
diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp
index 1de4e7f36..1f4a969fa 100644
--- a/launcher/meta/VersionList.cpp
+++ b/launcher/meta/VersionList.cpp
@@ -169,7 +169,7 @@ void VersionList::setName(const QString& name)
emit nameChanged(name);
}
-void VersionList::setVersions(const QVector& versions)
+void VersionList::setVersions(const QList& versions)
{
beginResetModel();
m_versions = versions;
@@ -265,7 +265,7 @@ void VersionList::setupAddedVersion(const int row, const Version::Ptr& version)
disconnect(version.get(), &Version::typeChanged, this, nullptr);
connect(version.get(), &Version::requiresChanged, this,
- [this, row]() { emit dataChanged(index(row), index(row), QVector() << RequiresRole); });
+ [this, row]() { emit dataChanged(index(row), index(row), QList() << RequiresRole); });
connect(version.get(), &Version::timeChanged, this,
[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 }); });
diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h
index 4215439db..21c86b751 100644
--- a/launcher/meta/VersionList.h
+++ b/launcher/meta/VersionList.h
@@ -61,14 +61,14 @@ class VersionList : public BaseVersionList, public BaseEntity {
Version::Ptr getVersion(const QString& version);
bool hasVersion(QString version) const;
- QVector versions() const { return m_versions; }
+ QList versions() const { return m_versions; }
// this blocks until the version list is loaded
void waitToLoad();
public: // for usage only by parsers
void setName(const QString& name);
- void setVersions(const QVector& versions);
+ void setVersions(const QList& versions);
void merge(const VersionList::Ptr& other);
void mergeFromIndex(const VersionList::Ptr& other);
void parse(const QJsonObject& obj) override;
@@ -82,7 +82,7 @@ class VersionList : public BaseVersionList, public BaseEntity {
void updateListData(QList) override {}
private:
- QVector m_versions;
+ QList m_versions;
QStringList m_externalRecommendsVersions;
QHash m_lookup;
QString m_uid;
diff --git a/launcher/minecraft/GradleSpecifier.h b/launcher/minecraft/GradleSpecifier.h
index 22db7d641..a2588064f 100644
--- a/launcher/minecraft/GradleSpecifier.h
+++ b/launcher/minecraft/GradleSpecifier.h
@@ -54,11 +54,11 @@ struct GradleSpecifier {
4 "jdk15"
5 "jar"
*/
- QRegularExpression matcher(
+ static const QRegularExpression s_matcher(
QRegularExpression::anchoredPattern("([^:@]+):([^:@]+):([^:@]+)"
"(?::([^:@]+))?"
"(?:@([^:@]+))?"));
- QRegularExpressionMatch match = matcher.match(value);
+ QRegularExpressionMatch match = s_matcher.match(value);
m_valid = match.hasMatch();
if (!m_valid) {
m_invalidValue = value;
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index 8cb0634a3..4d37aad9c 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -53,7 +53,6 @@
#include "MMCTime.h"
#include "java/JavaVersion.h"
#include "pathmatcher/MultiMatcher.h"
-#include "pathmatcher/RegexpMatcher.h"
#include "launch/LaunchTask.h"
#include "launch/TaskStepWrapper.h"
@@ -705,9 +704,9 @@ static QString replaceTokensIn(QString text, QMap with)
{
// TODO: does this still work??
QString result;
- QRegularExpression token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption);
+ static const QRegularExpression s_token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption);
QStringList list;
- QRegularExpressionMatchIterator i = token_regexp.globalMatch(text);
+ QRegularExpressionMatchIterator i = s_token_regexp.globalMatch(text);
int lastCapturedEnd = 0;
while (i.hasNext()) {
QRegularExpressionMatch match = i.next();
@@ -773,11 +772,7 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine
token_mapping["assets_root"] = absAssetsDir;
token_mapping["assets_index_name"] = assets->id;
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QStringList parts = args_pattern.split(' ', Qt::SkipEmptyParts);
-#else
- QStringList parts = args_pattern.split(' ', QString::SkipEmptyParts);
-#endif
for (int i = 0; i < parts.length(); i++) {
parts[i] = replaceTokensIn(parts[i], token_mapping);
}
@@ -832,11 +827,7 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftT
auto mainWindow = qobject_cast(w);
if (mainWindow) {
auto m = mainWindow->windowHandle()->frameMargins();
-#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
screenGeometry = screenGeometry.shrunkBy(m);
-#else
- screenGeometry = { screenGeometry.width() - m.left() - m.right(), screenGeometry.height() - m.top() - m.bottom() };
-#endif
break;
}
}
@@ -1028,61 +1019,10 @@ QMap MinecraftInstance::createCensorFilterFromSession(AuthSess
return filter;
}
-MessageLevel::Enum MinecraftInstance::guessLevel(const QString& line, MessageLevel::Enum level)
-{
- QRegularExpression re("\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]");
- 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()
+QStringList MinecraftInstance::getLogFileSearchPaths()
{
- auto combined = std::make_shared();
- combined->add(std::make_shared(".*\\.log(\\.[0-9]*)?(\\.gz)?$"));
- combined->add(std::make_shared("crash-.*\\.txt"));
- combined->add(std::make_shared("IDMap dump.*\\.txt$"));
- combined->add(std::make_shared("ModLoader\\.txt(\\..*)?$"));
- return combined;
-}
-
-QString MinecraftInstance::getLogFileRoot()
-{
- return gameRoot();
+ return { FS::PathCombine(gameRoot(), "crash-reports"), FS::PathCombine(gameRoot(), "logs"), gameRoot() };
}
QString MinecraftInstance::getStatusbarDescription()
diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h
index 68f5d4f2a..a37164169 100644
--- a/launcher/minecraft/MinecraftInstance.h
+++ b/launcher/minecraft/MinecraftInstance.h
@@ -142,12 +142,7 @@ class MinecraftInstance : public BaseInstance {
QProcessEnvironment createEnvironment() override;
QProcessEnvironment createLaunchEnvironment() override;
- /// guess log level from a line of minecraft log
- MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level) override;
-
- IPathMatcher::Ptr getLogFileMatcher() override;
-
- QString getLogFileRoot() override;
+ QStringList getLogFileSearchPaths() override;
QString getStatusbarDescription() override;
diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp
index 684869c8d..32dd1875c 100644
--- a/launcher/minecraft/OneSixVersionFormat.cpp
+++ b/launcher/minecraft/OneSixVersionFormat.cpp
@@ -114,9 +114,9 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc
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-_]+)*)")) };
- 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;
out->addProblem(ProblemSeverity::Error,
QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues."));
diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp
index d6534b910..8475a1f32 100644
--- a/launcher/minecraft/PackProfile.cpp
+++ b/launcher/minecraft/PackProfile.cpp
@@ -517,13 +517,9 @@ QVariant PackProfile::data(const QModelIndex& index, int role) const
switch (role) {
case Qt::CheckStateRole: {
- switch (column) {
- case NameColumn: {
- return patch->isEnabled() ? Qt::Checked : Qt::Unchecked;
- }
- default:
- return QVariant();
- }
+ if (column == NameColumn)
+ return patch->isEnabled() ? Qt::Checked : Qt::Unchecked;
+ return QVariant();
}
case Qt::DisplayRole: {
switch (column) {
@@ -649,11 +645,7 @@ void PackProfile::move(const int index, const MoveDirection direction)
return;
}
beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap);
-#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
d->components.swapItemsAt(index, theirIndex);
-#else
- d->components.swap(index, theirIndex);
-#endif
endMoveRows();
invalidateLaunchProfile();
scheduleSave();
diff --git a/launcher/minecraft/ProfileUtils.cpp b/launcher/minecraft/ProfileUtils.cpp
index 08ec0fac3..a79f89529 100644
--- a/launcher/minecraft/ProfileUtils.cpp
+++ b/launcher/minecraft/ProfileUtils.cpp
@@ -41,7 +41,6 @@
#include
#include
-#include
#include
namespace ProfileUtils {
diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp
index bd28f9e9a..8ae097bad 100644
--- a/launcher/minecraft/World.cpp
+++ b/launcher/minecraft/World.cpp
@@ -198,22 +198,6 @@ bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data)
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)
{
repath(file);
@@ -223,7 +207,6 @@ void World::repath(const QFileInfo& file)
{
m_containerFile = file;
m_folderName = file.fileName();
- m_size = calculateWorldSize(file);
if (file.isFile() && file.suffix() == "zip") {
m_iconFile = QString();
readFromZip(file);
@@ -252,41 +235,41 @@ void World::readFromFS(const QFileInfo& file)
{
auto bytes = getLevelDatDataFromFS(file);
if (bytes.isEmpty()) {
- is_valid = false;
+ m_isValid = false;
return;
}
loadFromLevelDat(bytes);
- levelDatTime = file.lastModified();
+ m_levelDatTime = file.lastModified();
}
void World::readFromZip(const QFileInfo& file)
{
QuaZip zip(file.absoluteFilePath());
- is_valid = zip.open(QuaZip::mdUnzip);
- if (!is_valid) {
+ m_isValid = zip.open(QuaZip::mdUnzip);
+ if (!m_isValid) {
return;
}
auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat");
- is_valid = !location.isEmpty();
- if (!is_valid) {
+ m_isValid = !location.isEmpty();
+ if (!m_isValid) {
return;
}
m_containerOffsetPath = location;
QuaZipFile zippedFile(&zip);
// read the install profile
- is_valid = zip.setCurrentFile(location + "level.dat");
- if (!is_valid) {
+ m_isValid = zip.setCurrentFile(location + "level.dat");
+ if (!m_isValid) {
return;
}
- is_valid = zippedFile.open(QIODevice::ReadOnly);
+ m_isValid = zippedFile.open(QIODevice::ReadOnly);
QuaZipFileInfo64 levelDatInfo;
zippedFile.getFileInfo(&levelDatInfo);
auto modTime = levelDatInfo.getNTFSmTime();
if (!modTime.isValid()) {
modTime = levelDatInfo.dateTime;
}
- levelDatTime = modTime;
- if (!is_valid) {
+ m_levelDatTime = modTime;
+ if (!m_isValid) {
return;
}
loadFromLevelDat(zippedFile.readAll());
@@ -430,7 +413,7 @@ void World::loadFromLevelDat(QByteArray data)
{
auto levelData = parseLevelDat(data);
if (!levelData) {
- is_valid = false;
+ m_isValid = false;
return;
}
@@ -439,20 +422,20 @@ void World::loadFromLevelDat(QByteArray data)
valPtr = &levelData->at("Data");
} catch (const std::out_of_range& e) {
qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what();
- is_valid = false;
+ m_isValid = false;
return;
}
nbt::value& val = *valPtr;
- is_valid = val.get_type() == nbt::tag_type::Compound;
- if (!is_valid)
+ m_isValid = val.get_type() == nbt::tag_type::Compound;
+ if (!m_isValid)
return;
auto name = read_string(val, "LevelName");
m_actualName = name ? *name : m_folderName;
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");
@@ -490,7 +473,7 @@ bool World::replace(World& with)
bool World::destroy()
{
- if (!is_valid)
+ if (!m_isValid)
return false;
if (FS::trash(m_containerFile.filePath()))
@@ -508,7 +491,7 @@ bool World::destroy()
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
@@ -531,3 +514,8 @@ bool World::isMoreThanOneHardLink() const
}
return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1;
}
+
+void World::setSize(int64_t size)
+{
+ m_size = size;
+}
diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h
index 4303dc553..34d418e79 100644
--- a/launcher/minecraft/World.h
+++ b/launcher/minecraft/World.h
@@ -39,7 +39,7 @@ class World {
QDateTime lastPlayed() const { return m_lastPlayed; }
GameType gameType() const { return m_gameType; }
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(); }
QFileInfo container() const { return m_containerFile; }
// delete all the files of this world
@@ -54,6 +54,8 @@ class World {
bool rename(const QString& to);
bool install(const QString& to, const QString& name = QString());
+ void setSize(int64_t size);
+
// WEAK compare operator - used for replacing worlds
bool operator==(const World& other) const;
@@ -83,10 +85,10 @@ class World {
QString m_folderName;
QString m_actualName;
QString m_iconFile;
- QDateTime levelDatTime;
+ QDateTime m_levelDatTime;
QDateTime m_lastPlayed;
int64_t m_size;
int64_t m_randomSeed = 0;
GameType m_gameType;
- bool is_valid = false;
+ bool m_isValid = false;
};
diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp
index 812b13c71..6a821ba60 100644
--- a/launcher/minecraft/WorldList.cpp
+++ b/launcher/minecraft/WorldList.cpp
@@ -37,13 +37,14 @@
#include
#include
+#include
#include
#include
#include
+#include
#include
#include
#include
-#include "Application.h"
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.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
m_watcher = new QFileSystemWatcher(this);
- is_watching = false;
+ m_isWatching = false;
connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &WorldList::directoryChanged);
}
void WorldList::startWatching()
{
- if (is_watching) {
+ if (m_isWatching) {
return;
}
update();
- is_watching = m_watcher->addPath(m_dir.absolutePath());
- if (is_watching) {
+ m_isWatching = m_watcher->addPath(m_dir.absolutePath());
+ if (m_isWatching) {
qDebug() << "Started watching " << m_dir.absolutePath();
} else {
qDebug() << "Failed to start watching " << m_dir.absolutePath();
@@ -71,11 +72,11 @@ void WorldList::startWatching()
void WorldList::stopWatching()
{
- if (!is_watching) {
+ if (!m_isWatching) {
return;
}
- is_watching = !m_watcher->removePath(m_dir.absolutePath());
- if (!is_watching) {
+ m_isWatching = !m_watcher->removePath(m_dir.absolutePath());
+ if (!m_isWatching) {
qDebug() << "Stopped watching " << m_dir.absolutePath();
} else {
qDebug() << "Failed to stop watching " << m_dir.absolutePath();
@@ -101,12 +102,13 @@ bool WorldList::update()
}
}
beginResetModel();
- worlds.swap(newWorlds);
+ m_worlds.swap(newWorlds);
endResetModel();
+ loadWorldsAsync();
return true;
}
-void WorldList::directoryChanged(QString path)
+void WorldList::directoryChanged(QString)
{
update();
}
@@ -123,12 +125,12 @@ QString WorldList::instDirPath() const
bool WorldList::deleteWorld(int index)
{
- if (index >= worlds.size() || index < 0)
+ if (index >= m_worlds.size() || index < 0)
return false;
- World& m = worlds[index];
+ World& m = m_worlds[index];
if (m.destroy()) {
beginRemoveRows(QModelIndex(), index, index);
- worlds.removeAt(index);
+ m_worlds.removeAt(index);
endRemoveRows();
emit changed();
return true;
@@ -139,11 +141,11 @@ bool WorldList::deleteWorld(int index)
bool WorldList::deleteWorlds(int first, int last)
{
for (int i = first; i <= last; i++) {
- World& m = worlds[i];
+ World& m = m_worlds[i];
m.destroy();
}
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();
emit changed();
return true;
@@ -151,9 +153,9 @@ bool WorldList::deleteWorlds(int first, int last)
bool WorldList::resetIcon(int row)
{
- if (row >= worlds.size() || row < 0)
+ if (row >= m_worlds.size() || row < 0)
return false;
- World& m = worlds[row];
+ World& m = m_worlds[row];
if (m.resetIcon()) {
emit dataChanged(index(row), index(row), { WorldList::IconFileRole });
return true;
@@ -174,12 +176,12 @@ QVariant WorldList::data(const QModelIndex& index, int role) const
int row = index.row();
int column = index.column();
- if (row < 0 || row >= worlds.size())
+ if (row < 0 || row >= m_worlds.size())
return QVariant();
QLocale locale;
- auto& world = worlds[row];
+ auto& world = m_worlds[row];
switch (role) {
case Qt::DisplayRole:
switch (column) {
@@ -208,13 +210,9 @@ QVariant WorldList::data(const QModelIndex& index, int role) const
}
case Qt::UserRole:
- switch (column) {
- case SizeColumn:
- return QVariant::fromValue(world.bytes());
-
- default:
- return data(index, Qt::DisplayRole);
- }
+ if (column == SizeColumn)
+ return QVariant::fromValue(world.bytes());
+ return data(index, Qt::DisplayRole);
case Qt::ToolTipRole: {
if (column == InfoColumn) {
@@ -311,11 +309,7 @@ class WorldMimeData : public QMimeData {
QStringList formats() const { return QMimeData::formats() << "text/uri-list"; }
protected:
-#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QVariant retrieveData(const QString& mimetype, QMetaType type) const
-#else
- QVariant retrieveData(const QString& mimetype, QVariant::Type type) const
-#endif
{
QList urls;
for (auto& world : m_worlds) {
@@ -343,9 +337,9 @@ QMimeData* WorldList::mimeData(const QModelIndexList& indexes) const
if (idx.column() != 0)
continue;
int row = idx.row();
- if (row < 0 || row >= this->worlds.size())
+ if (row < 0 || row >= this->m_worlds.size())
continue;
- worlds_.append(this->worlds[row]);
+ worlds_.append(this->m_worlds[row]);
}
if (!worlds_.size()) {
return new QMimeData();
@@ -397,7 +391,7 @@ bool WorldList::dropMimeData(const QMimeData* data,
return false;
// files dropped from outside?
if (data->hasUrls()) {
- bool was_watching = is_watching;
+ bool was_watching = m_isWatching;
if (was_watching)
stopWatching();
auto urls = data->urls();
@@ -420,4 +414,44 @@ bool WorldList::dropMimeData(const QMimeData* data,
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"
diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h
index bea24bb9a..93fecf1f5 100644
--- a/launcher/minecraft/WorldList.h
+++ b/launcher/minecraft/WorldList.h
@@ -40,9 +40,9 @@ class WorldList : public QAbstractListModel {
virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
virtual int columnCount(const QModelIndex& parent) const;
- size_t size() const { return worlds.size(); };
+ size_t size() const { return m_worlds.size(); };
bool empty() const { return size() == 0; }
- World& operator[](size_t index) { return worlds[index]; }
+ World& operator[](size_t index) { return m_worlds[index]; }
/// Reloads the mod list and returns true if the list changed.
virtual bool update();
@@ -82,10 +82,11 @@ class WorldList : public QAbstractListModel {
QString instDirPath() const;
- const QList