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

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad
2025-04-15 11:38:31 +01:00
118 changed files with 1541 additions and 1338 deletions

239
.github/workflows/blocked-prs.yml vendored Normal file
View File

@ -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 '<h3>PR Dependencies :pushpin:</h3>' > "$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<<EOF';
cat "${COMMENT_PATH}";
echo 'EOF';
} >> "$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

View File

@ -52,7 +52,7 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-20.04
- os: ubuntu-22.04
qt_ver: 5
qt_host: linux
qt_arch: ""
@ -63,8 +63,11 @@ jobs:
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 +109,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:
@ -168,9 +163,15 @@ jobs:
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.2
uses: actions/cache@v4.2.3
with:
path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
@ -187,11 +188,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
@ -249,14 +255,21 @@ jobs:
- name: Prepare AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5
env:
APPIMAGEUPDATE_HASH: ${{ matrix.appimageupdate_hash }}
LINUXDEPLOY_HASH: ${{ matrix.linuxdeploy_hash }}
LINUXDEPLOY_QT_HASH: ${{ matrix.linuxdeploy_qt_hash }}
run: |
wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/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'
@ -278,11 +291,6 @@ jobs:
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 +298,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'

56
.github/workflows/merge-blocking-pr.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: Merged Blocking Pull Request Automation
on:
pull_request:
types:
- closed
jobs:
update-blocked-status:
name: Update Blocked Status
runs-on: ubuntu-latest
# a pr that was a `blocking:<id>` label was merged.
# find the open pr's it was blocked by and trigger a refresh of their state
if: github.event.pull_request.merged == true && contains( join( 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: ${{ github.event.pull_request.number }}
run: |
blocked_prs=$(
gh -R ${{ github.repository }} pr list --label 'blocked' --json 'number,body' \
| jq -c --argjson pr "${{ github.event.pull_request.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")

View File

@ -2,19 +2,30 @@ name: Nix
on:
push:
tags:
- "*"
paths-ignore:
- ".github/**"
- "!.github/workflows/nix.yml"
- "flatpak/"
- "scripts/"
- ".git*"
- ".envrc"
- "**.md"
- "**/LICENSE"
- ".github/ISSUE_TEMPLATE/**"
- ".markdownlint**"
- "flatpak/**"
- "!COPYING.md"
- "renovate.json"
pull_request_target:
paths-ignore:
- ".github/**"
- "flatpak/"
- "scripts/"
- ".git*"
- ".envrc"
- "**.md"
- "**/LICENSE"
- ".github/ISSUE_TEMPLATE/**"
- ".markdownlint**"
- "flatpak/**"
- "!COPYING.md"
- "renovate.json"
workflow_dispatch:
permissions:
@ -60,7 +71,7 @@ jobs:
# 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 +87,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"

View File

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

View File

@ -49,7 +49,6 @@ jobs:
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 }}
@ -104,5 +103,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

View File

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

7
.gitignore vendored
View File

@ -21,6 +21,7 @@ CMakeCache.txt
/.vs
cmake-build-*/
Debug
compile_commands.json
# Build dirs
build
@ -47,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

3
.gitmodules vendored
View File

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

View File

@ -99,6 +99,12 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0")
# set CXXFLAGS for build targets
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}")
# Export compile commands for debug builds if we can (useful in LSPs like clangd)
# https://cmake.org/cmake/help/v3.31/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html
if(CMAKE_GENERATOR STREQUAL "Unix Makefiles" OR CMAKE_GENERATOR STREQUAL "Ninja" AND CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
endif()
option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF)
# If this is a Debug build turn on address sanitiser
@ -189,10 +195,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.")
@ -236,7 +243,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
@ -357,9 +364,6 @@ 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()
@ -377,7 +381,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))
@ -554,12 +558,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 ###############################

View File

@ -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 <s.schuemann@pobox.com>
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 <uri_herrera@nitrux.in> and others

View File

@ -34,8 +34,8 @@
*/
#include <qstringliteral.h>
#include "BuildConfig.h"
#include <QObject>
#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);
}

View File

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

View File

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

39
flake.lock generated
View File

@ -1,21 +1,5 @@
{
"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": {
@ -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": 1741851582,
"narHash": "sha256-cPfs8qMccim2RBgtKGF+x9IBCduRvd/N5F4nYpU0TVE=",
"lastModified": 1744463964,
"narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6607cf789e541e7873d40d3a8f7815ea92204f32",
"rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650",
"type": "github"
},
"original": {
@ -65,9 +34,7 @@
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"libnbtplusplus": "libnbtplusplus",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs"
}
}

151
flake.nix
View File

@ -15,28 +15,6 @@
url = "github:PrismLauncher/libnbtplusplus";
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";
flake = false;
};
};
outputs =
@ -44,9 +22,8 @@
self,
nixpkgs,
libnbtplusplus,
nix-filter,
...
}:
let
inherit (nixpkgs) lib;
@ -58,27 +35,128 @@
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; [
inputsFrom = [ packages'.prismlauncher-unwrapped ];
packages = with pkgs; [
ccache
ninja
llvm.clang-tools
];
cmakeBuildType = "Debug";
cmakeFlags = [ "-GNinja" ] ++ packages'.prismlauncher.cmakeFlags;
dontFixCmake = true;
shellHook = ''
echo "Sourcing ${qt-wrapper-env}"
source ${qt-wrapper-env}
git submodule update --init --force
if [ ! -f compile_commands.json ]; then
cmakeConfigurePhase
cd ..
ln -s "$cmakeBuildDir"/compile_commands.json compile_commands.json
fi
echo ${lib.escapeShellArg welcomeMessage}
'';
};
}
);
@ -89,7 +167,6 @@
prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix {
inherit
libnbtplusplus
nix-filter
self
;
};
@ -99,6 +176,7 @@
packages = forAllSystems (
system:
let
pkgs = nixpkgsFor.${system};
@ -111,6 +189,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
);
@ -118,16 +197,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;
};

View File

@ -376,19 +376,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 {
@ -408,10 +409,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;
}
}
@ -710,7 +717,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

View File

@ -386,6 +386,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();

View File

@ -126,6 +126,9 @@ class BaseInstance : public QObject, public std::enable_shared_from_this<BaseIns
QString name() const;
void setName(QString val);
/// Sync name and rename instance dir accordingly; returns true if successful
bool syncInstanceDirName(const QString& newRoot) const;
/// Value used for instance window titles
QString windowTitle() const;

View File

@ -21,6 +21,8 @@ set(CORE_SOURCES
BaseVersion.h
BaseInstance.h
BaseInstance.cpp
InstanceDirUpdate.h
InstanceDirUpdate.cpp
NullInstance.h
MMCZip.h
MMCZip.cpp
@ -1286,7 +1288,6 @@ target_link_libraries(Launcher_logic
qdcss
BuildConfig
Qt${QT_VERSION_MAJOR}::Widgets
ghcFilesystem::ghc_filesystem
)
if (UNIX AND NOT CYGWIN AND NOT APPLE)
@ -1373,7 +1374,6 @@ if(Launcher_BUILD_UPDATER)
${ZLIB_LIBRARIES}
systeminfo
BuildConfig
ghcFilesystem::ghc_filesystem
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network
@ -1412,7 +1412,6 @@ if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER))
target_link_libraries(filelink_logic
systeminfo
BuildConfig
ghcFilesystem::ghc_filesystem
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network

View File

@ -15,7 +15,7 @@
DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathMatcher)
: Task(), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath)
{
m_copy.matcher(m_pathMatcher.get()).whitelist(true);
m_copy.matcher(m_pathMatcher).whitelist(true);
}
void DataMigrationTask::executeTask()

View File

@ -77,24 +77,8 @@
#include <utime.h>
#endif
// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
#ifdef __APPLE__
#include <Availability.h> // 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(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
#define GHC_USE_STD_FS
#include <filesystem>
namespace fs = std::filesystem;
#endif // MacOS min version check
#endif // Other OSes version check
#ifndef GHC_USE_STD_FS
#include <ghc/filesystem.hpp>
namespace fs = ghc::filesystem;
#endif
// clone
#if defined(Q_OS_LINUX)
@ -950,7 +934,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";

View File

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

View File

@ -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());

View File

@ -28,7 +28,7 @@ class InstanceCopyTask : public InstanceTask {
InstancePtr m_origInstance;
QFuture<bool> m_copyFuture;
QFutureWatcher<bool> m_copyFutureWatcher;
std::unique_ptr<IPathMatcher> m_matcher;
IPathMatcher::Ptr m_matcher;
bool m_keepPlaytime;
bool m_useLinks = false;
bool m_useHardLinks = false;

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "InstanceDirUpdate.h"
#include <QCheckBox>
#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. <br />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: <br/>"
" - Old instance root: %1<br/>"
" - New instance root: %2<br/>"
"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;
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "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);

View File

@ -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();

View File

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

View File

@ -40,24 +40,8 @@
#include "WindowsConsole.h"
#endif
// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
#ifdef __APPLE__
#include <Availability.h> // 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(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
#define GHC_USE_STD_FS
#include <filesystem>
namespace fs = std::filesystem;
#endif // MacOS min version check
#endif // Other OSes version check
#ifndef GHC_USE_STD_FS
#include <ghc/filesystem.hpp>
namespace fs = ghc::filesystem;
#endif
FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this))
{

View File

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

View File

@ -208,13 +208,9 @@ QVariant WorldList::data(const QModelIndex& index, int role) const
}
case Qt::UserRole:
switch (column) {
case SizeColumn:
return QVariant::fromValue<qlonglong>(world.bytes());
default:
return data(index, Qt::DisplayRole);
}
if (column == SizeColumn)
return QVariant::fromValue<qlonglong>(world.bytes());
return data(index, Qt::DisplayRole);
case Qt::ToolTipRole: {
if (column == InfoColumn) {

View File

@ -260,6 +260,30 @@ int AccountList::count() const
return m_accounts.count();
}
QString getAccountStatus(AccountState status)
{
switch (status) {
case AccountState::Unchecked:
return QObject::tr("Unchecked", "Account status");
case AccountState::Offline:
return QObject::tr("Offline", "Account status");
case AccountState::Online:
return QObject::tr("Ready", "Account status");
case AccountState::Working:
return QObject::tr("Working", "Account status");
case AccountState::Errored:
return QObject::tr("Errored", "Account status");
case AccountState::Expired:
return QObject::tr("Expired", "Account status");
case AccountState::Disabled:
return QObject::tr("Disabled", "Account status");
case AccountState::Gone:
return QObject::tr("Gone", "Account status");
default:
return QObject::tr("Unknown", "Account status");
}
}
QVariant AccountList::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
@ -273,13 +297,10 @@ QVariant AccountList::data(const QModelIndex& index, int role) const
switch (role) {
case Qt::DisplayRole:
switch (index.column()) {
case ProfileNameColumn: {
case ProfileNameColumn:
return account->profileName();
}
case NameColumn:
return account->accountDisplayString();
case TypeColumn: {
switch (account->accountType()) {
case AccountType::MSA: {
@ -291,39 +312,8 @@ QVariant AccountList::data(const QModelIndex& index, int role) const
}
return tr("Unknown", "Account type");
}
case StatusColumn: {
switch (account->accountState()) {
case AccountState::Unchecked: {
return tr("Unchecked", "Account status");
}
case AccountState::Offline: {
return tr("Offline", "Account status");
}
case AccountState::Online: {
return tr("Ready", "Account status");
}
case AccountState::Working: {
return tr("Working", "Account status");
}
case AccountState::Errored: {
return tr("Errored", "Account status");
}
case AccountState::Expired: {
return tr("Expired", "Account status");
}
case AccountState::Disabled: {
return tr("Disabled", "Account status");
}
case AccountState::Gone: {
return tr("Gone", "Account status");
}
default: {
return tr("Unknown", "Account status");
}
}
}
case StatusColumn:
return getAccountStatus(account->accountState());
default:
return QVariant();
}
@ -335,11 +325,9 @@ QVariant AccountList::data(const QModelIndex& index, int role) const
return QVariant::fromValue(account);
case Qt::CheckStateRole:
if (index.column() == ProfileNameColumn) {
if (index.column() == ProfileNameColumn)
return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked;
} else {
return QVariant();
}
return QVariant();
default:
return QVariant();
@ -461,18 +449,14 @@ bool AccountList::loadList()
// Make sure the format version matches.
auto listVersion = root.value("formatVersion").toVariant().toInt();
switch (listVersion) {
case AccountListVersion::MojangMSA: {
return loadV3(root);
} break;
default: {
QString newName = "accounts-old.json";
qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName;
// Attempt to rename the old version.
file.rename(newName);
return false;
}
}
if (listVersion == AccountListVersion::MojangMSA)
return loadV3(root);
QString newName = "accounts-old.json";
qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName;
// Attempt to rename the old version.
file.rename(newName);
return false;
}
bool AccountList::loadV3(QJsonObject& root)

View File

@ -1,5 +1,4 @@
#include <QDebug>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>

View File

@ -1,12 +1,9 @@
#pragma once
#include <QMultiMap>
#include <QString>
#include <memory>
#include "QObjectPtr.h"
class MinecraftAccount;
class QNetworkAccessManager;
struct AuthSession {
bool MakeOffline(QString offline_playername);

View File

@ -87,16 +87,12 @@ QVariant GameOptions::data(const QModelIndex& index, int role) const
if (row < 0 || row >= int(contents.size()))
return QVariant();
switch (role) {
case Qt::DisplayRole:
if (column == 0) {
return contents[row].key;
} else {
return contents[row].value;
}
default:
return QVariant();
if (role == Qt::DisplayRole) {
if (column == 0)
return contents[row].key;
return contents[row].value;
}
return QVariant();
}
int GameOptions::rowCount(const QModelIndex&) const

View File

@ -105,19 +105,16 @@ std::pair<Version, Version> DataPack::compatibleVersions() const
int DataPack::compare(const Resource& other, SortType type) const
{
auto const& cast_other = static_cast<DataPack const&>(other);
switch (type) {
default:
return Resource::compare(other, type);
case SortType::PACK_FORMAT: {
auto this_ver = packFormat();
auto other_ver = cast_other.packFormat();
if (type == SortType::PACK_FORMAT) {
auto this_ver = packFormat();
auto other_ver = cast_other.packFormat();
if (this_ver > other_ver)
return 1;
if (this_ver < other_ver)
return -1;
break;
}
if (this_ver > other_ver)
return 1;
if (this_ver < other_ver)
return -1;
} else {
return Resource::compare(other, type);
}
return 0;
}

View File

@ -35,8 +35,6 @@ class Version;
class DataPack : public Resource {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<Resource>;
DataPack(QObject* parent = nullptr) : Resource(parent) {}
DataPack(QFileInfo file_info) : Resource(file_info) {}
@ -59,6 +57,8 @@ class DataPack : public Resource {
[[nodiscard]] int compare(Resource const& other, SortType type) const override;
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
virtual QString directory() { return "/data"; }
protected:
mutable QMutex m_data_lock;

View File

@ -138,6 +138,15 @@ auto Mod::name() const -> QString
return Resource::name();
}
auto Mod::mod_id() const -> QString
{
auto d_mod_id = details().mod_id;
if (!d_mod_id.isEmpty())
return d_mod_id;
return Resource::name();
}
auto Mod::version() const -> QString
{
return details().version;

View File

@ -61,6 +61,7 @@ class Mod : public Resource {
auto details() const -> const ModDetails&;
auto name() const -> QString override;
auto mod_id() const -> QString;
auto version() const -> QString;
auto homepage() const -> QString override;
auto description() const -> QString;

View File

@ -145,12 +145,9 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const
}
return {};
case Qt::CheckStateRole:
switch (column) {
case ActiveColumn:
return at(row).enabled() ? Qt::Checked : Qt::Unchecked;
default:
return QVariant();
}
if (column == ActiveColumn)
return at(row).enabled() ? Qt::Checked : Qt::Unchecked;
return QVariant();
default:
return QVariant();
}

View File

@ -152,6 +152,9 @@ class Resource : public QObject {
[[nodiscard]] bool isMoreThanOneHardLink() const;
[[nodiscard]] auto mod_id() const -> QString { return m_mod_id; }
void setModId(const QString& modId) { m_mod_id = modId; }
protected:
/* The file corresponding to this resource. */
QFileInfo m_file_info;
@ -162,6 +165,7 @@ class Resource : public QObject {
QString m_internal_id;
/* Name as reported via the file name. In the absence of a better name, this is shown to the user. */
QString m_name;
QString m_mod_id;
/* The type of file we're dealing with. */
ResourceType m_type = ResourceType::UNKNOWN;

View File

@ -513,12 +513,9 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
return {};
}
case Qt::CheckStateRole:
switch (column) {
case ActiveColumn:
return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
default:
return {};
}
if (column == ActiveColumn)
return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
return {};
default:
return {};
}

View File

@ -42,13 +42,6 @@ void ResourcePack::setPackFormat(int new_format_id)
m_pack_format = new_format_id;
}
void ResourcePack::setDescription(QString new_description)
{
QMutexLocker locker(&m_data_lock);
m_description = new_description;
}
void ResourcePack::setImage(QImage new_image) const
{
QMutexLocker locker(&m_data_lock);
@ -90,7 +83,7 @@ QPixmap ResourcePack::image(QSize size, Qt::AspectRatioMode mode) const
}
// Imaged got evicted from the cache. Re-process it and retry.
ResourcePackUtils::processPackPNG(*this);
ResourcePackUtils::processPackPNG(this);
return image(size);
}
@ -102,44 +95,3 @@ std::pair<Version, Version> ResourcePack::compatibleVersions() const
return s_pack_format_versions.constFind(m_pack_format).value();
}
int ResourcePack::compare(const Resource& other, SortType type) const
{
auto const& cast_other = static_cast<ResourcePack const&>(other);
switch (type) {
default:
return Resource::compare(other, type);
case SortType::PACK_FORMAT: {
auto this_ver = packFormat();
auto other_ver = cast_other.packFormat();
if (this_ver > other_ver)
return 1;
if (this_ver < other_ver)
return -1;
break;
}
}
return 0;
}
bool ResourcePack::applyFilter(QRegularExpression filter) const
{
if (filter.match(description()).hasMatch())
return true;
if (filter.match(QString::number(packFormat())).hasMatch())
return true;
if (filter.match(compatibleVersions().first.toString()).hasMatch())
return true;
if (filter.match(compatibleVersions().second.toString()).hasMatch())
return true;
return Resource::applyFilter(filter);
}
bool ResourcePack::valid() const
{
return m_pack_format != 0;
}

View File

@ -1,6 +1,7 @@
#pragma once
#include "Resource.h"
#include "minecraft/mod/DataPack.h"
#include <QImage>
#include <QMutex>
@ -14,51 +15,27 @@ class Version;
* Store localized descriptions
* */
class ResourcePack : public Resource {
class ResourcePack : public DataPack {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<Resource>;
ResourcePack(QObject* parent = nullptr) : DataPack(parent) {}
ResourcePack(QFileInfo file_info) : DataPack(file_info) {}
ResourcePack(QObject* parent = nullptr) : Resource(parent) {}
ResourcePack(QFileInfo file_info) : Resource(file_info) {}
/** Gets the numerical ID of the pack format. */
[[nodiscard]] int packFormat() const { return m_pack_format; }
/** Gets, respectively, the lower and upper versions supported by the set pack format. */
[[nodiscard]] std::pair<Version, Version> compatibleVersions() const;
/** Gets the description of the resource pack. */
[[nodiscard]] QString description() const { return m_description; }
/** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */
[[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const;
/** Thread-safe. */
void setPackFormat(int new_format_id);
/** Thread-safe. */
void setDescription(QString new_description);
/** Thread-safe. */
void setImage(QImage new_image) const;
bool valid() const override;
/** Thread-safe. */
void setPackFormat(int new_format_id);
[[nodiscard]] int compare(Resource const& other, SortType type) const override;
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
virtual QString directory() { return "/assets"; }
protected:
mutable QMutex m_data_lock;
/* The 'version' of a resource pack, as defined in the pack.mcmeta file.
* See https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
*/
int m_pack_format = 0;
/** The resource pack's description, as defined in the pack.mcmeta file.
*/
QString m_description;
/** The resource pack's image file cache key, for access in the QPixmapCache global instance.
*
* The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true),

View File

@ -44,7 +44,7 @@
#include "Application.h"
#include "Version.h"
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
#include "minecraft/mod/tasks/LocalDataPackParseTask.h"
ResourcePackFolderModel::ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent)
: ResourceFolderModel(dir, instance, is_indexed, create_dir, parent)
@ -128,12 +128,9 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
}
return {};
case Qt::CheckStateRole:
switch (column) {
case ActiveColumn:
return at(row).enabled() ? Qt::Checked : Qt::Unchecked;
default:
return {};
}
if (column == ActiveColumn)
return at(row).enabled() ? Qt::Checked : Qt::Unchecked;
return {};
default:
return {};
}
@ -191,5 +188,5 @@ int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const
Task* ResourcePackFolderModel::createParseTask(Resource& resource)
{
return new LocalResourcePackParseTask(m_next_resolution_ticket, static_cast<ResourcePack&>(resource));
return new LocalDataPackParseTask(m_next_resolution_ticket, dynamic_cast<ResourcePack*>(&resource));
}

View File

@ -23,6 +23,8 @@
#include "FileSystem.h"
#include "Json.h"
#include "minecraft/mod/ResourcePack.h"
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
@ -32,9 +34,9 @@
namespace DataPackUtils {
bool process(DataPack& pack, ProcessingLevel level)
bool process(DataPack* pack, ProcessingLevel level)
{
switch (pack.type()) {
switch (pack->type()) {
case ResourceType::FOLDER:
return DataPackUtils::processFolder(pack, level);
case ResourceType::ZIPFILE:
@ -45,16 +47,16 @@ bool process(DataPack& pack, ProcessingLevel level)
}
}
bool processFolder(DataPack& pack, ProcessingLevel level)
bool processFolder(DataPack* pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::FOLDER);
Q_ASSERT(pack->type() == ResourceType::FOLDER);
auto mcmeta_invalid = [&pack]() {
qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta";
qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta";
return false; // the mcmeta is not optional
};
QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta"));
QFileInfo mcmeta_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.mcmeta"));
if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) {
QFile mcmeta_file(mcmeta_file_info.filePath());
if (!mcmeta_file.open(QIODevice::ReadOnly))
@ -72,7 +74,7 @@ bool processFolder(DataPack& pack, ProcessingLevel level)
return mcmeta_invalid(); // mcmeta file isn't a valid file
}
QFileInfo data_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "data"));
QFileInfo data_dir_info(FS::PathCombine(pack->fileinfo().filePath(), pack->directory()));
if (!data_dir_info.exists() || !data_dir_info.isDir()) {
return false; // data dir does not exists or isn't valid
}
@ -80,22 +82,46 @@ bool processFolder(DataPack& pack, ProcessingLevel level)
if (level == ProcessingLevel::BasicInfoOnly) {
return true; // only need basic info already checked
}
if (auto rp = dynamic_cast<ResourcePack*>(pack)) {
auto png_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png";
return true; // the png is optional
};
QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png"));
if (image_file_info.exists() && image_file_info.isFile()) {
QFile pack_png_file(image_file_info.filePath());
if (!pack_png_file.open(QIODevice::ReadOnly))
return png_invalid(); // can't open pack.png file
auto data = pack_png_file.readAll();
bool pack_png_result = ResourcePackUtils::processPackPNG(rp, std::move(data));
pack_png_file.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
return png_invalid(); // pack.png does not exists or is not a valid file.
}
}
return true; // all tests passed
}
bool processZIP(DataPack& pack, ProcessingLevel level)
bool processZIP(DataPack* pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
Q_ASSERT(pack->type() == ResourceType::ZIPFILE);
QuaZip zip(pack.fileinfo().filePath());
QuaZip zip(pack->fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return false; // can't open zip file
QuaZipFile file(&zip);
auto mcmeta_invalid = [&pack]() {
qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta";
qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta";
return false; // the mcmeta is not optional
};
@ -119,7 +145,7 @@ bool processZIP(DataPack& pack, ProcessingLevel level)
}
QuaZipDir zipDir(&zip);
if (!zipDir.exists("/data")) {
if (!zipDir.exists(pack->directory())) {
return false; // data dir does not exists at zip root
}
@ -127,21 +153,49 @@ bool processZIP(DataPack& pack, ProcessingLevel level)
zip.close();
return true; // only need basic info already checked
}
if (auto rp = dynamic_cast<ResourcePack*>(pack)) {
auto png_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png";
return true; // the png is optional
};
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return png_invalid();
}
auto data = file.readAll();
bool pack_png_result = ResourcePackUtils::processPackPNG(rp, std::move(data));
file.close();
zip.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
zip.close();
return png_invalid(); // could not set pack.mcmeta as current file.
}
}
zip.close();
return true;
}
// https://minecraft.wiki/w/Data_pack#pack.mcmeta
bool processMCMeta(DataPack& pack, QByteArray&& raw_data)
// https://minecraft.wiki/w/Raw_JSON_text_format
// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
bool processMCMeta(DataPack* pack, QByteArray&& raw_data)
{
try {
auto json_doc = QJsonDocument::fromJson(raw_data);
auto pack_obj = Json::requireObject(json_doc.object(), "pack", {});
pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0));
pack.setDescription(Json::ensureString(pack_obj, "description", ""));
pack->setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0));
pack->setDescription(ResourcePackUtils::processComponent(pack_obj.value("description")));
} catch (Json::JsonException& e) {
qWarning() << "JsonException: " << e.what() << e.cause();
return false;
@ -152,26 +206,19 @@ bool processMCMeta(DataPack& pack, QByteArray&& raw_data)
bool validate(QFileInfo file)
{
DataPack dp{ file };
return DataPackUtils::process(dp, ProcessingLevel::BasicInfoOnly) && dp.valid();
return DataPackUtils::process(&dp, ProcessingLevel::BasicInfoOnly) && dp.valid();
}
} // namespace DataPackUtils
LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack& dp) : Task(false), m_token(token), m_data_pack(dp) {}
bool LocalDataPackParseTask::abort()
{
m_aborted = true;
return true;
}
LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack* dp) : Task(false), m_token(token), m_data_pack(dp) {}
void LocalDataPackParseTask::executeTask()
{
if (!DataPackUtils::process(m_data_pack))
if (!DataPackUtils::process(m_data_pack)) {
emitFailed("process failed");
return;
}
if (m_aborted)
emitAborted();
else
emitSucceeded();
emitSucceeded();
}

View File

@ -32,12 +32,12 @@ namespace DataPackUtils {
enum class ProcessingLevel { Full, BasicInfoOnly };
bool process(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool process(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full);
bool processZIP(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processFolder(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processZIP(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full);
bool processFolder(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full);
bool processMCMeta(DataPack& pack, QByteArray&& raw_data);
bool processMCMeta(DataPack* pack, QByteArray&& raw_data);
/** Checks whether a file is valid as a data pack or not. */
bool validate(QFileInfo file);
@ -47,10 +47,7 @@ bool validate(QFileInfo file);
class LocalDataPackParseTask : public Task {
Q_OBJECT
public:
LocalDataPackParseTask(int token, DataPack& dp);
[[nodiscard]] bool canAbort() const override { return true; }
bool abort() override;
LocalDataPackParseTask(int token, DataPack* dp);
void executeTask() override;
@ -59,7 +56,5 @@ class LocalDataPackParseTask : public Task {
private:
int m_token;
DataPack& m_data_pack;
bool m_aborted = false;
DataPack* m_data_pack;
};

View File

@ -20,6 +20,7 @@
#include "FileSystem.h"
#include "Json.h"
#include "minecraft/mod/tasks/LocalDataPackParseTask.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
@ -29,155 +30,6 @@
namespace ResourcePackUtils {
bool process(ResourcePack& pack, ProcessingLevel level)
{
switch (pack.type()) {
case ResourceType::FOLDER:
return ResourcePackUtils::processFolder(pack, level);
case ResourceType::ZIPFILE:
return ResourcePackUtils::processZIP(pack, level);
default:
qWarning() << "Invalid type for resource pack parse task!";
return false;
}
}
bool processFolder(ResourcePack& pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::FOLDER);
auto mcmeta_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta";
return false; // the mcmeta is not optional
};
QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta"));
if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) {
QFile mcmeta_file(mcmeta_file_info.filePath());
if (!mcmeta_file.open(QIODevice::ReadOnly))
return mcmeta_invalid(); // can't open mcmeta file
auto data = mcmeta_file.readAll();
bool mcmeta_result = ResourcePackUtils::processMCMeta(pack, std::move(data));
mcmeta_file.close();
if (!mcmeta_result) {
return mcmeta_invalid(); // mcmeta invalid
}
} else {
return mcmeta_invalid(); // mcmeta file isn't a valid file
}
QFileInfo assets_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "assets"));
if (!assets_dir_info.exists() || !assets_dir_info.isDir()) {
return false; // assets dir does not exists or isn't valid
}
if (level == ProcessingLevel::BasicInfoOnly) {
return true; // only need basic info already checked
}
auto png_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png";
return true; // the png is optional
};
QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
if (image_file_info.exists() && image_file_info.isFile()) {
QFile pack_png_file(image_file_info.filePath());
if (!pack_png_file.open(QIODevice::ReadOnly))
return png_invalid(); // can't open pack.png file
auto data = pack_png_file.readAll();
bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data));
pack_png_file.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
return png_invalid(); // pack.png does not exists or is not a valid file.
}
return true; // all tests passed
}
bool processZIP(ResourcePack& pack, ProcessingLevel level)
{
Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
QuaZip zip(pack.fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return false; // can't open zip file
QuaZipFile file(&zip);
auto mcmeta_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta";
return false; // the mcmeta is not optional
};
if (zip.setCurrentFile("pack.mcmeta")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return mcmeta_invalid();
}
auto data = file.readAll();
bool mcmeta_result = ResourcePackUtils::processMCMeta(pack, std::move(data));
file.close();
if (!mcmeta_result) {
return mcmeta_invalid(); // mcmeta invalid
}
} else {
return mcmeta_invalid(); // could not set pack.mcmeta as current file.
}
QuaZipDir zipDir(&zip);
if (!zipDir.exists("/assets")) {
return false; // assets dir does not exists at zip root
}
if (level == ProcessingLevel::BasicInfoOnly) {
zip.close();
return true; // only need basic info already checked
}
auto png_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png";
return true; // the png is optional
};
if (zip.setCurrentFile("pack.png")) {
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file in zip.";
zip.close();
return png_invalid();
}
auto data = file.readAll();
bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data));
file.close();
zip.close();
if (!pack_png_result) {
return png_invalid(); // pack.png invalid
}
} else {
zip.close();
return png_invalid(); // could not set pack.mcmeta as current file.
}
zip.close();
return true;
}
QString buildStyle(const QJsonObject& obj)
{
QStringList styles;
@ -259,30 +111,11 @@ QString processComponent(const QJsonValue& value, bool strikethrough, bool under
return {};
}
// https://minecraft.wiki/w/Raw_JSON_text_format
// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
{
try {
auto json_doc = QJsonDocument::fromJson(raw_data);
auto pack_obj = Json::requireObject(json_doc.object(), "pack", {});
pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0));
pack.setDescription(processComponent(pack_obj.value("description")));
} catch (Json::JsonException& e) {
qWarning() << "JsonException: " << e.what() << e.cause();
return false;
}
return true;
}
bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data)
bool processPackPNG(const ResourcePack* pack, QByteArray&& raw_data)
{
auto img = QImage::fromData(raw_data);
if (!img.isNull()) {
pack.setImage(img);
pack->setImage(img);
} else {
qWarning() << "Failed to parse pack.png.";
return false;
@ -290,16 +123,16 @@ bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data)
return true;
}
bool processPackPNG(const ResourcePack& pack)
bool processPackPNG(const ResourcePack* pack)
{
auto png_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png";
qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png";
return false;
};
switch (pack.type()) {
switch (pack->type()) {
case ResourceType::FOLDER: {
QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png"));
if (image_file_info.exists() && image_file_info.isFile()) {
QFile pack_png_file(image_file_info.filePath());
if (!pack_png_file.open(QIODevice::ReadOnly))
@ -319,7 +152,7 @@ bool processPackPNG(const ResourcePack& pack)
return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740
}
case ResourceType::ZIPFILE: {
QuaZip zip(pack.fileinfo().filePath());
QuaZip zip(pack->fileinfo().filePath());
if (!zip.open(QuaZip::mdUnzip))
return false; // can't open zip file
@ -353,28 +186,7 @@ bool processPackPNG(const ResourcePack& pack)
bool validate(QFileInfo file)
{
ResourcePack rp{ file };
return ResourcePackUtils::process(rp, ProcessingLevel::BasicInfoOnly) && rp.valid();
return DataPackUtils::process(&rp, DataPackUtils::ProcessingLevel::BasicInfoOnly) && rp.valid();
}
} // namespace ResourcePackUtils
LocalResourcePackParseTask::LocalResourcePackParseTask(int token, ResourcePack& rp) : Task(false), m_token(token), m_resource_pack(rp) {}
bool LocalResourcePackParseTask::abort()
{
m_aborted = true;
return true;
}
void LocalResourcePackParseTask::executeTask()
{
if (!ResourcePackUtils::process(m_resource_pack)) {
emitFailed("this is not a resource pack");
return;
}
if (m_aborted)
emitAborted();
else
emitSucceeded();
}

View File

@ -23,44 +23,14 @@
#include "minecraft/mod/ResourcePack.h"
#include "tasks/Task.h"
namespace ResourcePackUtils {
enum class ProcessingLevel { Full, BasicInfoOnly };
bool process(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full);
QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false);
bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data);
bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data);
bool processPackPNG(const ResourcePack* pack, QByteArray&& raw_data);
/// processes ONLY the pack.png (rest of the pack may be invalid)
bool processPackPNG(const ResourcePack& pack);
bool processPackPNG(const ResourcePack* pack);
/** Checks whether a file is valid as a resource pack or not. */
bool validate(QFileInfo file);
} // namespace ResourcePackUtils
class LocalResourcePackParseTask : public Task {
Q_OBJECT
public:
LocalResourcePackParseTask(int token, ResourcePack& rp);
[[nodiscard]] bool canAbort() const override { return true; }
bool abort() override;
void executeTask() override;
[[nodiscard]] int token() const { return m_token; }
private:
int m_token;
ResourcePack& m_resource_pack;
bool m_aborted = false;
};

View File

@ -180,8 +180,10 @@ bool LocalWorldSaveParseTask::abort()
void LocalWorldSaveParseTask::executeTask()
{
if (!WorldSaveUtils::process(m_save))
if (!WorldSaveUtils::process(m_save)) {
emitFailed("this is not a world");
return;
}
if (m_aborted)
emitAborted();

View File

@ -62,7 +62,7 @@ class UserInteractionSupport {
/**
* Requests a user interaction to select which optional mods should be installed.
*/
virtual std::optional<QVector<QString>> chooseOptionalMods(PackVersion version, QVector<ATLauncher::VersionMod> mods) = 0;
virtual std::optional<QVector<QString>> chooseOptionalMods(const PackVersion& version, QVector<ATLauncher::VersionMod> mods) = 0;
/**
* Requests a user interaction to select a component version from a given version list

View File

@ -20,7 +20,6 @@
#include <algorithm>
#include "Json.h"
#include "QObjectPtr.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
@ -33,9 +32,7 @@
static const FlameAPI flameAPI;
static ModrinthAPI modrinthAPI;
Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess)
: m_network(network), m_manifest(toProcess)
{}
Flame::FileResolvingTask::FileResolvingTask(Flame::Manifest& toProcess) : m_manifest(toProcess) {}
bool Flame::FileResolvingTask::abort()
{

View File

@ -17,8 +17,6 @@
*/
#pragma once
#include <QNetworkAccessManager>
#include "PackManifest.h"
#include "tasks/Task.h"
@ -26,7 +24,7 @@ namespace Flame {
class FileResolvingTask : public Task {
Q_OBJECT
public:
explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess);
explicit FileResolvingTask(Flame::Manifest& toProcess);
virtual ~FileResolvingTask() = default;
bool canAbort() const override { return true; }
@ -44,7 +42,6 @@ class FileResolvingTask : public Task {
void getFlameProjects();
private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
Flame::Manifest m_manifest;
std::shared_ptr<QByteArray> m_result;
Task::Ptr m_task;

View File

@ -102,57 +102,6 @@ QString FlameAPI::getModDescription(int modId)
return description;
}
QList<ModPlatform::IndexedVersion> FlameAPI::getLatestVersions(VersionSearchArgs&& args)
{
auto versions_url_optional = getVersionsURL(args);
if (!versions_url_optional.has_value())
return {};
auto versions_url = versions_url_optional.value();
QEventLoop loop;
auto netJob = makeShared<NetJob>(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network());
auto response = std::make_shared<QByteArray>();
QList<ModPlatform::IndexedVersion> ver;
netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
QObject::connect(netJob.get(), &NetJob::succeeded, [response, args, &ver] {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
try {
auto obj = Json::requireObject(doc);
auto arr = Json::requireArray(obj, "data");
for (auto file : arr) {
auto file_obj = Json::requireObject(file);
ver.append(FlameMod::loadIndexedPackVersion(file_obj));
}
} catch (Json::JsonException& e) {
qCritical() << "Failed to parse response from a version request.";
qCritical() << e.what();
qDebug() << doc;
}
});
QObject::connect(netJob.get(), &NetJob::finished, &loop, &QEventLoop::quit);
netJob->start();
loop.exec();
return ver;
}
Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const
{
auto netJob = makeShared<NetJob>(QString("Flame::GetProjects"), APPLICATION->network());
@ -266,7 +215,7 @@ QList<ModPlatform::Category> FlameAPI::loadModCategories(std::shared_ptr<QByteAr
return categories;
};
std::optional<ModPlatform::IndexedVersion> FlameAPI::getLatestVersion(QList<ModPlatform::IndexedVersion> versions,
std::optional<ModPlatform::IndexedVersion> FlameAPI::getLatestVersion(QVector<ModPlatform::IndexedVersion> versions,
QList<ModPlatform::ModLoaderType> instanceLoaders,
ModPlatform::ModLoaderTypes modLoaders)
{

View File

@ -15,8 +15,7 @@ class FlameAPI : public NetworkResourceAPI {
QString getModFileChangelog(int modId, int fileId);
QString getModDescription(int modId);
QList<ModPlatform::IndexedVersion> getLatestVersions(VersionSearchArgs&& args);
std::optional<ModPlatform::IndexedVersion> getLatestVersion(QList<ModPlatform::IndexedVersion> versions,
std::optional<ModPlatform::IndexedVersion> getLatestVersion(QVector<ModPlatform::IndexedVersion> versions,
QList<ModPlatform::ModLoaderType> instanceLoaders,
ModPlatform::ModLoaderTypes fallback);
@ -108,12 +107,6 @@ class FlameAPI : public NetworkResourceAPI {
return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&');
}
private:
[[nodiscard]] std::optional<QString> getInfoURL(QString const& id) const override
{
return QString("https://api.curseforge.com/v1/mods/%1").arg(id);
}
[[nodiscard]] std::optional<QString> getVersionsURL(VersionSearchArgs const& args) const override
{
auto addonId = args.pack.addonId.toString();
@ -129,6 +122,11 @@ class FlameAPI : public NetworkResourceAPI {
return url;
}
private:
[[nodiscard]] std::optional<QString> getInfoURL(QString const& id) const override
{
return QString("https://api.curseforge.com/v1/mods/%1").arg(id);
}
[[nodiscard]] std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
{
auto addonId = args.dependency.addonId.toString();

View File

@ -3,115 +3,31 @@
#include "FlameAPI.h"
#include "FlameModIndex.h"
#include <MurmurHash2.h>
#include <QHash>
#include <memory>
#include "Json.h"
#include "QObjectPtr.h"
#include "ResourceDownloadTask.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/tasks/GetModDependenciesTask.h"
#include "modplatform/ModIndex.h"
#include "net/ApiDownload.h"
#include "net/NetJob.h"
#include "tasks/Task.h"
static FlameAPI api;
bool FlameCheckUpdate::abort()
{
m_was_aborted = true;
if (m_net_job)
return m_net_job->abort();
return true;
}
ModPlatform::IndexedPack FlameCheckUpdate::getProjectInfo(ModPlatform::IndexedVersion& ver_info)
{
ModPlatform::IndexedPack pack;
QEventLoop loop;
auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network());
auto response = std::make_shared<QByteArray>();
auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString());
auto dl = Net::ApiDownload::makeByteArray(url, response);
get_project_job->addNetAction(dl);
QObject::connect(get_project_job, &NetJob::succeeded, [response, &pack]() {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
try {
auto doc_obj = Json::requireObject(doc);
auto data_obj = Json::requireObject(doc_obj, "data");
FlameMod::loadIndexedPack(pack, data_obj);
} catch (Json::JsonException& e) {
qWarning() << e.cause();
qDebug() << doc;
}
});
connect(get_project_job, &NetJob::failed, this, &FlameCheckUpdate::emitFailed);
QObject::connect(get_project_job, &NetJob::finished, [&loop, get_project_job] {
get_project_job->deleteLater();
loop.quit();
});
get_project_job->start();
loop.exec();
return pack;
}
ModPlatform::IndexedVersion FlameCheckUpdate::getFileInfo(int addonId, int fileId)
{
ModPlatform::IndexedVersion ver;
QEventLoop loop;
auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network());
auto response = std::make_shared<QByteArray>();
auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId));
auto dl = Net::ApiDownload::makeByteArray(url, response);
get_file_info_job->addNetAction(dl);
QObject::connect(get_file_info_job, &NetJob::succeeded, [response, &ver]() {
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
try {
auto doc_obj = Json::requireObject(doc);
auto data_obj = Json::requireObject(doc_obj, "data");
ver = FlameMod::loadIndexedPackVersion(data_obj);
} catch (Json::JsonException& e) {
qWarning() << e.cause();
qDebug() << doc;
}
});
connect(get_file_info_job, &NetJob::failed, this, &FlameCheckUpdate::emitFailed);
QObject::connect(get_file_info_job, &NetJob::finished, [&loop, get_file_info_job] {
get_file_info_job->deleteLater();
loop.quit();
});
get_file_info_job->start();
loop.exec();
return ver;
bool result = false;
if (m_task && m_task->canAbort()) {
result = m_task->abort();
}
Task::abort();
return result;
}
/* Check for update:
@ -123,66 +39,163 @@ void FlameCheckUpdate::executeTask()
{
setStatus(tr("Preparing resources for CurseForge..."));
int i = 0;
auto netJob = new NetJob("Get latest versions", APPLICATION->network());
connect(netJob, &Task::finished, this, &FlameCheckUpdate::collectBlockedMods);
connect(netJob, &Task::progress, this, &FlameCheckUpdate::setProgress);
connect(netJob, &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress);
connect(netJob, &Task::details, this, &FlameCheckUpdate::setDetails);
for (auto* resource : m_resources) {
setStatus(tr("Getting API response from CurseForge for '%1'...").arg(resource->name()));
setProgress(i++, m_resources.size());
auto latest_vers = api.getLatestVersions({ { resource->metadata()->project_id.toString() }, m_game_versions });
// Check if we were aborted while getting the latest version
if (m_was_aborted) {
aborted();
return;
}
auto latest_ver = api.getLatestVersion(latest_vers, m_loaders_list, resource->metadata()->loaders);
setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name()));
if (!latest_ver.has_value() || !latest_ver->addonId.isValid()) {
QString reason;
if (dynamic_cast<Mod*>(resource) != nullptr)
reason =
tr("No valid version found for this resource. It's probably unavailable for the current game "
"version / mod loader.");
else
reason = tr("No valid version found for this resource. It's probably unavailable for the current game version.");
emit checkFailed(resource, reason);
auto versions_url_optional = api.getVersionsURL({ { resource->metadata()->project_id.toString() }, m_game_versions });
if (!versions_url_optional.has_value())
continue;
}
if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != resource->metadata()->file_id) {
auto pack = getProjectInfo(latest_ver.value());
auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver->fileId.toString());
emit checkFailed(resource, tr("Resource has a new update available, but is not downloadable using CurseForge."), recover_url);
auto response = std::make_shared<QByteArray>();
auto task = Net::ApiDownload::makeByteArray(versions_url_optional.value(), response);
continue;
}
connect(task.get(), &Task::succeeded, this, [this, resource, response] { getLatestVersionCallback(resource, response); });
netJob->addNetAction(task);
}
m_task.reset(netJob);
m_task->start();
}
// Fake pack with the necessary info to pass to the download task :)
auto pack = std::make_shared<ModPlatform::IndexedPack>();
pack->name = resource->name();
pack->slug = resource->metadata()->slug;
pack->addonId = resource->metadata()->project_id;
pack->provider = ModPlatform::ResourceProvider::FLAME;
if (!latest_ver->hash.isEmpty() &&
(resource->metadata()->hash != latest_ver->hash || resource->status() == ResourceStatus::NOT_INSTALLED)) {
auto old_version = resource->metadata()->version_number;
if (old_version.isEmpty()) {
if (resource->status() == ResourceStatus::NOT_INSTALLED)
old_version = tr("Not installed");
else
old_version = tr("Unknown");
}
auto download_task = makeShared<ResourceDownloadTask>(pack, latest_ver.value(), m_resource_model);
m_updates.emplace_back(pack->name, resource->metadata()->hash, old_version, latest_ver->version, latest_ver->version_type,
api.getModFileChangelog(latest_ver->addonId.toInt(), latest_ver->fileId.toInt()),
ModPlatform::ResourceProvider::FLAME, download_task, resource->enabled());
}
m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, latest_ver.value()));
void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, std::shared_ptr<QByteArray> response)
{
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
emitSucceeded();
// Fake pack with the necessary info to pass to the download task :)
auto pack = std::make_shared<ModPlatform::IndexedPack>();
pack->name = resource->name();
pack->slug = resource->metadata()->slug;
pack->addonId = resource->metadata()->project_id;
pack->provider = ModPlatform::ResourceProvider::FLAME;
try {
auto obj = Json::requireObject(doc);
auto arr = Json::requireArray(obj, "data");
FlameMod::loadIndexedPackVersions(*pack.get(), arr);
} catch (Json::JsonException& e) {
qCritical() << "Failed to parse response from a version request.";
qCritical() << e.what();
qDebug() << doc;
}
auto latest_ver = api.getLatestVersion(pack->versions, m_loaders_list, resource->metadata()->loaders);
setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name()));
if (!latest_ver.has_value() || !latest_ver->addonId.isValid()) {
QString reason;
if (dynamic_cast<Mod*>(resource) != nullptr)
reason =
tr("No valid version found for this resource. It's probably unavailable for the current game "
"version / mod loader.");
else
reason = tr("No valid version found for this resource. It's probably unavailable for the current game version.");
emit checkFailed(resource, reason);
return;
}
if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != resource->metadata()->file_id) {
m_blocked[resource] = latest_ver->fileId.toString();
return;
}
if (!latest_ver->hash.isEmpty() &&
(resource->metadata()->hash != latest_ver->hash || resource->status() == ResourceStatus::NOT_INSTALLED)) {
auto old_version = resource->metadata()->version_number;
if (old_version.isEmpty()) {
if (resource->status() == ResourceStatus::NOT_INSTALLED)
old_version = tr("Not installed");
else
old_version = tr("Unknown");
}
auto download_task = makeShared<ResourceDownloadTask>(pack, latest_ver.value(), m_resource_model);
m_updates.emplace_back(pack->name, resource->metadata()->hash, old_version, latest_ver->version, latest_ver->version_type,
api.getModFileChangelog(latest_ver->addonId.toInt(), latest_ver->fileId.toInt()),
ModPlatform::ResourceProvider::FLAME, download_task, resource->enabled());
}
m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, latest_ver.value()));
}
void FlameCheckUpdate::collectBlockedMods()
{
QStringList addonIds;
QHash<QString, Resource*> quickSearch;
for (auto const& resource : m_blocked.keys()) {
auto addonId = resource->metadata()->project_id.toString();
addonIds.append(addonId);
quickSearch[addonId] = resource;
}
auto response = std::make_shared<QByteArray>();
Task::Ptr projTask;
if (addonIds.isEmpty()) {
emitSucceeded();
return;
} else if (addonIds.size() == 1) {
projTask = api.getProject(*addonIds.begin(), response);
} else {
projTask = api.getProjects(addonIds, response);
}
connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds, quickSearch] {
QJsonParseError parse_error{};
auto doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Flame projects task at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
try {
QJsonArray entries;
if (addonIds.size() == 1)
entries = { Json::requireObject(Json::requireObject(doc), "data") };
else
entries = Json::requireArray(Json::requireObject(doc), "data");
for (auto entry : entries) {
auto entry_obj = Json::requireObject(entry);
auto id = QString::number(Json::requireInteger(entry_obj, "id"));
auto resource = quickSearch.find(id).value();
ModPlatform::IndexedPack pack;
try {
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name()));
FlameMod::loadIndexedPack(pack, entry_obj);
auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, m_blocked[resource]);
emit checkFailed(resource, tr("Resource has a new update available, but is not downloadable using CurseForge."),
recover_url);
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << entries;
}
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << doc;
}
});
connect(projTask.get(), &Task::finished, this, &FlameCheckUpdate::emitSucceeded); // do not care much about error
connect(projTask.get(), &Task::progress, this, &FlameCheckUpdate::setProgress);
connect(projTask.get(), &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress);
connect(projTask.get(), &Task::details, this, &FlameCheckUpdate::setDetails);
m_task.reset(projTask);
m_task->start();
}

View File

@ -1,7 +1,6 @@
#pragma once
#include "modplatform/CheckUpdateTask.h"
#include "net/NetJob.h"
class FlameCheckUpdate : public CheckUpdateTask {
Q_OBJECT
@ -19,12 +18,12 @@ class FlameCheckUpdate : public CheckUpdateTask {
protected slots:
void executeTask() override;
private slots:
void getLatestVersionCallback(Resource* resource, std::shared_ptr<QByteArray> response);
void collectBlockedMods();
private:
ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info);
ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId);
Task::Ptr m_task = nullptr;
NetJob* m_net_job = nullptr;
bool m_was_aborted = false;
QHash<Resource*, QString> m_blocked;
};

View File

@ -442,7 +442,7 @@ bool FlameCreationTask::createInstance()
instance.setName(name());
m_modIdResolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack));
m_modIdResolver.reset(new Flame::FileResolvingTask(m_pack));
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); });
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) {
m_modIdResolver.reset();

View File

@ -76,10 +76,7 @@ static QString enumToString(int hash_algorithm)
}
}
void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
[[maybe_unused]] const shared_qobject_ptr<QNetworkAccessManager>& network,
const BaseInstance* inst)
void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr)
{
QVector<ModPlatform::IndexedVersion> unsortedVersions;
for (auto versionIter : arr) {

View File

@ -6,7 +6,6 @@
#include "modplatform/ModIndex.h"
#include <QNetworkAccessManager>
#include "BaseInstance.h"
namespace FlameMod {
@ -14,10 +13,7 @@ namespace FlameMod {
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
const BaseInstance* inst);
auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion;
auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion;
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr);
ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false);
ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst);
} // namespace FlameMod

View File

@ -203,6 +203,7 @@ QString exportToModList(QList<Mod*> mods, QString lineTemplate)
for (auto mod : mods) {
auto meta = mod->metadata();
auto modName = mod->name();
auto modID = mod->mod_id();
auto url = mod->homepage();
auto ver = mod->version();
if (ver.isEmpty() && meta != nullptr)
@ -211,6 +212,7 @@ QString exportToModList(QList<Mod*> mods, QString lineTemplate)
auto filename = mod->fileinfo().fileName();
lines << QString(lineTemplate)
.replace("{name}", modName)
.replace("{mod_id}", modID)
.replace("{url}", url)
.replace("{version}", ver)
.replace("{authors}", authors)

View File

@ -40,7 +40,7 @@ class PackFetchTask : public QObject {
void failed(QString reason);
void aborted();
void privateFileDownloadFinished(Modpack modpack);
void privateFileDownloadFinished(const Modpack& modpack);
void privateFileDownloadFailed(QString reason, QString packCode);
};

View File

@ -52,7 +52,7 @@
namespace LegacyFTB {
PackInstallTask::PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, Modpack pack, QString version)
PackInstallTask::PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, const Modpack& pack, QString version)
{
m_pack = pack;
m_version = version;

View File

@ -18,7 +18,7 @@ class PackInstallTask : public InstanceTask {
Q_OBJECT
public:
explicit PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, Modpack pack, QString version);
explicit PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, const Modpack& pack, QString version);
virtual ~PackInstallTask() {}
bool canAbort() const override { return true; }

View File

@ -112,7 +112,7 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
pack.extraDataLoaded = true;
}
void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst)
void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr)
{
QVector<ModPlatform::IndexedVersion> unsortedVersions;
for (auto versionIter : arr) {

View File

@ -19,14 +19,13 @@
#include "modplatform/ModIndex.h"
#include <QNetworkAccessManager>
#include "BaseInstance.h"
namespace Modrinth {
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst);
void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr);
auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion;
auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion;

View File

@ -54,7 +54,7 @@ Task::State FileSink::init(QNetworkRequest& request)
return Task::State::Failed;
}
wroteAnyData = false;
m_wroteAnyData = false;
m_output_file.reset(new PSaveFile(m_filename));
if (!m_output_file->open(QIODevice::WriteOnly)) {
qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing";
@ -72,17 +72,19 @@ Task::State FileSink::write(QByteArray& data)
qCCritical(taskNetLogC) << "Failed writing into " + m_filename;
m_output_file->cancelWriting();
m_output_file.reset();
wroteAnyData = false;
m_wroteAnyData = false;
return Task::State::Failed;
}
wroteAnyData = true;
m_wroteAnyData = true;
return Task::State::Running;
}
Task::State FileSink::abort()
{
m_output_file->cancelWriting();
if (m_output_file) {
m_output_file->cancelWriting();
}
failAllValidators();
return Task::State::Failed;
}
@ -100,7 +102,7 @@ Task::State FileSink::finalize(QNetworkReply& reply)
// if we wrote any data to the save file, we try to commit the data to the real file.
// if it actually got a proper file, we write it even if it was empty
if (gotFile || wroteAnyData) {
if (gotFile || m_wroteAnyData) {
// ask validators for data consistency
// we only do this for actual downloads, not 'your data is still the same' cache hits
if (!finalizeAllValidators(reply))

View File

@ -58,7 +58,7 @@ class FileSink : public Sink {
protected:
QString m_filename;
bool wroteAnyData = false;
bool m_wroteAnyData = false;
std::unique_ptr<PSaveFile> m_output_file;
};
} // namespace Net

View File

@ -78,7 +78,7 @@ Task::State MetaCacheSink::finalizeCache(QNetworkReply& reply)
{
QFileInfo output_file_info(m_filename);
if (wroteAnyData) {
if (m_wroteAnyData) {
m_entry->setMD5Sum(m_md5Node->hash().toHex().constData());
}

View File

@ -80,7 +80,7 @@ void NetRequest::executeTask()
emit finished();
return;
case State::Running:
qCDebug(logCat) << getUid().toString() << "Runninng " << m_url.toString();
qCDebug(logCat) << getUid().toString() << "Running " << m_url.toString();
break;
case State::Inactive:
case State::Failed:

View File

@ -1,3 +1,5 @@
#pragma once
#include <SeparatorPrefixTree.h>
#include <QRegularExpression>
#include "IPathMatcher.h"

View File

@ -1,3 +1,5 @@
#pragma once
#include <QRegularExpression>
#include "IPathMatcher.h"

View File

@ -123,6 +123,7 @@
#include "KonamiCode.h"
#include "InstanceCopyTask.h"
#include "InstanceDirUpdate.h"
#include "Json.h"
@ -288,10 +289,27 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi
view->setSelectionMode(QAbstractItemView::SingleSelection);
// FIXME: leaks ListViewDelegate
view->setItemDelegate(new ListViewDelegate(this));
auto delegate = new ListViewDelegate(this);
view->setItemDelegate(delegate);
view->setFrameShape(QFrame::NoFrame);
// do not show ugly blue border on the mac
view->setAttribute(Qt::WA_MacShowFocusRect, false);
connect(delegate, &ListViewDelegate::textChanged, this, [this](QString before, QString after) {
if (auto newRoot = askToUpdateInstanceDirName(m_selectedInstance, before, after, this); !newRoot.isEmpty()) {
auto oldID = m_selectedInstance->id();
auto newID = QFileInfo(newRoot).fileName();
QString origGroup(APPLICATION->instances()->getInstanceGroup(oldID));
bool syncGroup = origGroup != GroupId() && oldID != newID;
if (syncGroup)
APPLICATION->instances()->setInstanceGroup(oldID, GroupId());
refreshInstances();
setSelectedInstanceById(newID);
if (syncGroup)
APPLICATION->instances()->setInstanceGroup(newID, origGroup);
}
});
view->installEventFilter(this);
view->setContextMenuPolicy(Qt::CustomContextMenu);
@ -1377,20 +1395,8 @@ void MainWindow::on_actionDeleteInstance_triggered()
if (response != QMessageBox::Yes)
return;
auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id);
if (!linkedInstances.empty()) {
response = CustomMessageBox::selectable(this, tr("There are linked instances"),
tr("The following instance(s) might reference files in this instance:\n\n"
"%1\n\n"
"Deleting it could break the other instance(s), \n\n"
"Do you wish to proceed?",
nullptr, linkedInstances.count())
.arg(linkedInstances.join("\n")),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes)
return;
}
if (!checkLinkedInstances(id, this, tr("Deleting")))
return;
if (APPLICATION->instances()->trashInstance(id)) {
ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething());

View File

@ -21,7 +21,8 @@ QMessageBox* selectable(QWidget* parent,
const QString& text,
QMessageBox::Icon icon,
QMessageBox::StandardButtons buttons,
QMessageBox::StandardButton defaultButton)
QMessageBox::StandardButton defaultButton,
QCheckBox* checkBox)
{
QMessageBox* messageBox = new QMessageBox(parent);
messageBox->setWindowTitle(title);
@ -31,6 +32,8 @@ QMessageBox* selectable(QWidget* parent,
messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse);
messageBox->setIcon(icon);
messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction);
if (checkBox)
messageBox->setCheckBox(checkBox);
return messageBox;
}

View File

@ -23,5 +23,6 @@ QMessageBox* selectable(QWidget* parent,
const QString& text,
QMessageBox::Icon icon = QMessageBox::NoIcon,
QMessageBox::StandardButtons buttons = QMessageBox::Ok,
QMessageBox::StandardButton defaultButton = QMessageBox::NoButton);
QMessageBox::StandardButton defaultButton = QMessageBox::NoButton,
QCheckBox* checkBox = nullptr);
}

View File

@ -79,6 +79,9 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>This text supports the following placeholders:&#10;{name} - Mod name&#10;{mod_id} - Mod ID&#10;{url} - Mod URL&#10;{version} - Mod version&#10;{authors} - Mod authors</string>
</property>
</widget>
</item>
</layout>

View File

@ -92,7 +92,7 @@ ProgressDialog::~ProgressDialog()
{
for (auto conn : this->m_taskConnections) {
disconnect(conn);
}
}
delete ui;
}

View File

@ -93,7 +93,7 @@ class ProgressDialog : public QDialog {
Ui::ProgressDialog* ui;
Task* m_task;
QList<QMetaObject::Connection> m_taskConnections;
bool m_is_multi_step = false;

View File

@ -282,6 +282,7 @@ auto ResourceUpdateDialog::ensureMetadata() -> bool
bool skip_rest = false;
ModPlatform::ResourceProvider provider_rest = ModPlatform::ResourceProvider::MODRINTH;
// adds resource to list based on provider
auto addToTmp = [&modrinth_tmp, &flame_tmp](Resource* resource, ModPlatform::ResourceProvider p) {
switch (p) {
case ModPlatform::ResourceProvider::MODRINTH:
@ -293,6 +294,7 @@ auto ResourceUpdateDialog::ensureMetadata() -> bool
}
};
// ask the user on what provider to seach for the mod first
for (auto candidate : m_candidates) {
if (candidate->status() != ResourceStatus::NO_METADATA) {
onMetadataEnsured(candidate);
@ -335,6 +337,7 @@ auto ResourceUpdateDialog::ensureMetadata() -> bool
addToTmp(candidate, response.chosen);
}
// prepare task for the modrinth mods
if (!modrinth_tmp.empty()) {
auto modrinth_task = makeShared<EnsureMetadataTask>(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH);
connect(modrinth_task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); });
@ -350,6 +353,7 @@ auto ResourceUpdateDialog::ensureMetadata() -> bool
seq.addTask(modrinth_task);
}
// prepare task for the flame mods
if (!flame_tmp.empty()) {
auto flame_task = makeShared<EnsureMetadataTask>(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME);
connect(flame_task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); });
@ -367,6 +371,7 @@ auto ResourceUpdateDialog::ensureMetadata() -> bool
seq.addTask(m_second_try_metadata);
// execute all the tasks
ProgressDialog checking_dialog(m_parent);
checking_dialog.setSkipButton(true, tr("Abort"));
checking_dialog.setWindowTitle(tr("Generating metadata..."));
@ -477,13 +482,8 @@ void ResourceUpdateDialog::appendResource(CheckUpdateTask::Update const& info, Q
auto changelog_area = new QTextBrowser();
QString text = info.changelog;
switch (info.provider) {
case ModPlatform::ResourceProvider::MODRINTH: {
text = markdownToHTML(info.changelog.toUtf8());
break;
}
default:
break;
if (info.provider == ModPlatform::ResourceProvider::MODRINTH) {
text = markdownToHTML(info.changelog.toUtf8());
}
changelog_area->setHtml(StringUtils::htmlListPatch(text));

View File

@ -223,6 +223,8 @@ void SkinManageDialog::on_capeCombo_currentIndexChanged(int index)
auto cape = m_capes.value(id.toString(), {});
if (!cape.isNull()) {
m_ui->capeImage->setPixmap(previewCape(cape).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation));
} else {
m_ui->capeImage->clear();
}
m_skinPreview->updateCape(cape);
if (auto skin = getSelectedSkin(); skin) {
@ -522,6 +524,8 @@ void SkinManageDialog::resizeEvent(QResizeEvent* event)
auto cape = m_capes.value(id.toString(), {});
if (!cape.isNull()) {
m_ui->capeImage->setPixmap(previewCape(cape).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation));
} else {
m_ui->capeImage->clear();
}
}

View File

@ -397,6 +397,7 @@ void ListViewDelegate::setModelData(QWidget* editor, QAbstractItemModel* model,
// Prevent instance names longer than 128 chars
text.truncate(128);
if (text.size() != 0) {
emit textChanged(model->data(index).toString(), text);
model->setData(index, text);
}
}

View File

@ -33,6 +33,9 @@ class ListViewDelegate : public QStyledItemDelegate {
void setEditorData(QWidget* editor, const QModelIndex& index) const override;
void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override;
signals:
void textChanged(QString before, QString after) const;
private slots:
void editingDone();
};

View File

@ -66,7 +66,7 @@ class AccountListPage : public QMainWindow, public BasePage {
return icon;
}
QString id() const override { return "accounts"; }
QString helpPage() const override { return "/getting-started/adding-an-account"; }
QString helpPage() const override { return "getting-started/adding-an-account"; }
void retranslate() override;
public slots:

View File

@ -65,6 +65,15 @@ enum InstSortMode {
Sort_LastLaunch
};
enum InstRenamingMode {
// Rename metadata only.
Rename_Always,
// Ask everytime.
Rename_Ask,
// Rename physical directory too.
Rename_Never
};
LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherPage)
{
ui->setupUi(this);
@ -221,6 +230,7 @@ void LauncherPage::applySettings()
s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked());
s->set("MoveModsFromDownloadsDir", ui->downloadsDirMoveCheckBox->isChecked());
// Instance
auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId();
switch (sortMode) {
case Sort_LastLaunch:
@ -232,6 +242,20 @@ void LauncherPage::applySettings()
break;
}
auto renamingMode = (InstRenamingMode)ui->renamingBehaviorComboBox->currentIndex();
switch (renamingMode) {
case Rename_Always:
s->set("InstRenamingMode", "MetadataOnly");
break;
case Rename_Never:
s->set("InstRenamingMode", "PhysicalDir");
break;
case Rename_Ask:
default:
s->set("InstRenamingMode", "AskEverytime");
break;
}
// Mods
s->set("ModMetadataDisabled", !ui->metadataEnableBtn->isChecked());
s->set("ModDependenciesDisabled", !ui->dependenciesEnableBtn->isChecked());
@ -267,14 +291,25 @@ void LauncherPage::loadSettings()
ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool());
ui->downloadsDirMoveCheckBox->setChecked(s->get("MoveModsFromDownloadsDir").toBool());
// Instance
QString sortMode = s->get("InstSortMode").toString();
if (sortMode == "LastLaunch") {
ui->sortLastLaunchedBtn->setChecked(true);
} else {
ui->sortByNameBtn->setChecked(true);
}
QString renamingMode = s->get("InstRenamingMode").toString();
InstRenamingMode renamingModeEnum;
if (renamingMode == "MetadataOnly") {
renamingModeEnum = Rename_Always;
} else if (renamingMode == "PhysicalDir") {
renamingModeEnum = Rename_Never;
} else {
renamingModeEnum = Rename_Ask;
}
ui->renamingBehaviorComboBox->setCurrentIndex(renamingModeEnum);
// Mods
ui->metadataEnableBtn->setChecked(!s->get("ModMetadataDisabled").toBool());
ui->metadataWarningLabel->setHidden(ui->metadataEnableBtn->isChecked());

View File

@ -43,7 +43,7 @@
<x>0</x>
<y>0</y>
<width>575</width>
<height>1294</height>
<height>1368</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
@ -99,6 +99,51 @@
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="renamingBehaviorLabel">
<property name="text">
<string>When renaming instances...</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="renamingBehaviorComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Ask what to do with the folder</string>
</property>
</item>
<item>
<property name="text">
<string>Always rename the folder</string>
</property>
</item>
<item>
<property name="text">
<string>Never rename the folder - only the displayed name</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>6</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="preferMenuBarCheckBox">
<property name="toolTip">
@ -185,29 +230,16 @@
<string>Folders</string>
</property>
<layout class="QGridLayout" name="foldersBoxLayout">
<item row="3" column="0">
<widget class="QLabel" name="labelJavaDir">
<item row="2" column="0">
<widget class="QLabel" name="labelIconsDir">
<property name="text">
<string>&amp;Java</string>
<string>&amp;Icons</string>
</property>
<property name="buddy">
<cstring>javaDirTextBox</cstring>
<cstring>iconsDirTextBox</cstring>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="labelInstDir">
<property name="text">
<string>I&amp;nstances</string>
</property>
<property name="buddy">
<cstring>instDirTextBox</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="iconsDirTextBox"/>
</item>
<item row="8" column="0">
<widget class="QLabel" name="labelDownloadsDir">
<property name="text">
@ -218,56 +250,13 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelIconsDir">
<item row="3" column="0">
<widget class="QLabel" name="labelJavaDir">
<property name="text">
<string>&amp;Icons</string>
<string>&amp;Java</string>
</property>
<property name="buddy">
<cstring>iconsDirTextBox</cstring>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="skinsDirTextBox"/>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="modsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="javaDirTextBox"/>
</item>
<item row="8" column="1">
<widget class="QLineEdit" name="downloadsDirTextBox"/>
</item>
<item row="8" column="2">
<widget class="QPushButton" name="downloadsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="modsDirTextBox"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelModsDir">
<property name="text">
<string>&amp;Mods</string>
</property>
<property name="buddy">
<cstring>modsDirTextBox</cstring>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="javaDirBrowseBtn">
<property name="text">
<string>Browse</string>
<cstring>javaDirTextBox</cstring>
</property>
</widget>
</item>
@ -281,19 +270,18 @@
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="instDirBrowseBtn">
<item row="0" column="0">
<widget class="QLabel" name="labelInstDir">
<property name="text">
<string>Browse</string>
<string>I&amp;nstances</string>
</property>
<property name="buddy">
<cstring>instDirTextBox</cstring>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="iconsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
<item row="3" column="1">
<widget class="QLineEdit" name="javaDirTextBox"/>
</item>
<item row="4" column="2">
<widget class="QPushButton" name="skinsDirBrowseBtn">
@ -302,9 +290,66 @@
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QLineEdit" name="downloadsDirTextBox"/>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="iconsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="skinsDirTextBox"/>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="instDirTextBox"/>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="iconsDirTextBox"/>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="instDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="modsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="8" column="2">
<widget class="QPushButton" name="downloadsDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="javaDirBrowseBtn">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelModsDir">
<property name="text">
<string>&amp;Mods</string>
</property>
<property name="buddy">
<cstring>modsDirTextBox</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="modsDirTextBox"/>
</item>
</layout>
</widget>
</item>
@ -620,7 +665,6 @@
<tabstop>downloadsDirBrowseBtn</tabstop>
<tabstop>metadataEnableBtn</tabstop>
<tabstop>dependenciesEnableBtn</tabstop>
<tabstop>sortLastLaunchedBtn</tabstop>
<tabstop>sortByNameBtn</tabstop>
</tabstops>
<resources/>

View File

@ -245,7 +245,6 @@ ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWin
}
// MODRINTH
void ModrinthManagedPackPage::parseManagedPack()
{
qDebug() << "Parsing Modrinth pack";
@ -338,6 +337,25 @@ void ModrinthManagedPackPage::suggestVersion()
ManagedPackPage::suggestVersion();
}
/// @brief Called when the update task has completed.
/// Internally handles the closing of the instance window if the update was successful and shows a message box.
/// @param did_succeed Whether the update task was successful.
void ManagedPackPage::onUpdateTaskCompleted(bool did_succeed) const
{
// Close the window if the update was successful
if (did_succeed) {
if (m_instance_window != nullptr)
m_instance_window->close();
CustomMessageBox::selectable(nullptr, tr("Update Successful"), tr("The instance updated to pack version %1 successfully.").arg(m_inst->getManagedPackVersionName()), QMessageBox::Information)
->show();
} else {
CustomMessageBox::selectable(nullptr, tr("Update Failed"), tr("The instance failed to update to pack version %1. Please check launcher logs for more information.").arg(m_inst->getManagedPackVersionName()), QMessageBox::Critical)
->show();
}
}
void ModrinthManagedPackPage::update()
{
auto index = ui->versionsComboBox->currentIndex();
@ -363,10 +381,9 @@ void ModrinthManagedPackPage::update()
extracted->setIcon(m_inst->iconKey());
extracted->setConfirmUpdate(false);
// Run our task then handle the result
auto did_succeed = runUpdateTask(extracted);
if (m_instance_window && did_succeed)
m_instance_window->close();
onUpdateTaskCompleted(did_succeed);
}
void ModrinthManagedPackPage::updateFromFile()
@ -386,14 +403,12 @@ void ModrinthManagedPackPage::updateFromFile()
extracted->setIcon(m_inst->iconKey());
extracted->setConfirmUpdate(false);
// Run our task then handle the result
auto did_succeed = runUpdateTask(extracted);
if (m_instance_window && did_succeed)
m_instance_window->close();
onUpdateTaskCompleted(did_succeed);
}
// FLAME
FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent)
: ManagedPackPage(inst, instance_window, parent)
{
@ -531,9 +546,7 @@ void FlameManagedPackPage::update()
extracted->setConfirmUpdate(false);
auto did_succeed = runUpdateTask(extracted);
if (m_instance_window && did_succeed)
m_instance_window->close();
onUpdateTaskCompleted(did_succeed);
}
void FlameManagedPackPage::updateFromFile()
@ -555,8 +568,6 @@ void FlameManagedPackPage::updateFromFile()
extracted->setConfirmUpdate(false);
auto did_succeed = runUpdateTask(extracted);
if (m_instance_window && did_succeed)
m_instance_window->close();
onUpdateTaskCompleted(did_succeed);
}
#include "ManagedPackPage.moc"

View File

@ -94,6 +94,8 @@ class ManagedPackPage : public QWidget, public BasePage {
BaseInstance* m_inst;
bool m_loaded = false;
void onUpdateTaskCompleted(bool did_succeed) const;
};
/** Simple page for when we aren't a managed pack. */

View File

@ -36,9 +36,9 @@
*/
#include "ServersPage.h"
#include "ServerPingTask.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui_ServersPage.h"
#include "ServerPingTask.h"
#include <FileSystem.h>
#include <io/stream_reader.h>
@ -49,10 +49,10 @@
#include <tag_string.h>
#include <sstream>
#include <tasks/ConcurrentTask.h>
#include <QFileSystemWatcher>
#include <QMenu>
#include <QTimer>
#include <tasks/ConcurrentTask.h>
static const int COLUMN_COUNT = 3; // 3 , TBD: latency and other nice things.
@ -113,8 +113,8 @@ struct Server {
// Data - temporary
bool m_checked = false;
bool m_up = false;
QString m_motd; // https://mctools.org/motd-creator
std::optional<int> m_currentPlayers; // nullopt if not calculated/calculating
QString m_motd; // https://mctools.org/motd-creator
std::optional<int> m_currentPlayers; // nullopt if not calculated/calculating
int m_maxPlayers = 0;
};
@ -317,10 +317,10 @@ class ServersModel : public QAbstractListModel {
if (row < 0 || row >= m_servers.size())
return QVariant();
switch (column) {
case 0:
switch (role) {
case Qt::DecorationRole: {
switch (role) {
case Qt::DecorationRole: {
switch (column) {
case 0: {
auto& bytes = m_servers[row].m_icon;
if (bytes.size()) {
QPixmap px;
@ -329,31 +329,32 @@ class ServersModel : public QAbstractListModel {
}
return APPLICATION->getThemedIcon("unknown_server");
}
case Qt::DisplayRole:
return m_servers[row].m_name;
case ServerPtrRole:
return QVariant::fromValue<void*>((void*)&m_servers[row]);
default:
return QVariant();
}
case 1:
switch (role) {
case Qt::DisplayRole:
case 1:
return m_servers[row].m_address;
default:
return QVariant();
}
case 2:
switch (role) {
case Qt::DisplayRole:
case 2:
if (role == Qt::DisplayRole) {
if (m_servers[row].m_currentPlayers) {
return *m_servers[row].m_currentPlayers;
} else {
return "...";
}
default:
} else {
return QVariant();
}
}
}
case Qt::DisplayRole:
if (column == 0)
return m_servers[row].m_name;
else
return QVariant();
case ServerPtrRole:
if (column == 0)
return QVariant::fromValue<void*>((void*)&m_servers[row]);
else
return QVariant();
default:
return QVariant();
}
@ -447,22 +448,22 @@ class ServersModel : public QAbstractListModel {
}
m_currentQueryTask = ConcurrentTask::Ptr(
new ConcurrentTask("Query servers status", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())
);
new ConcurrentTask("Query servers status", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()));
int row = 0;
for (Server &server : m_servers) {
for (Server& server : m_servers) {
// reset current players
server.m_currentPlayers = {};
emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1));
// Start task to query server status
auto target = MinecraftTarget::parse(server.m_address, false);
auto *task = new ServerPingTask(target.address, target.port);
auto* task = new ServerPingTask(target.address, target.port);
m_currentQueryTask->addTask(Task::Ptr(task));
// Update the model when the task is done
connect(task, &Task::finished, this, [this, task, row]() {
if (m_servers.size() < row) return;
if (m_servers.size() < row)
return;
m_servers[row].m_currentPlayers = task->m_outputOnlinePlayers;
emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1));
});
@ -717,7 +718,7 @@ void ServersPage::openedImpl()
ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray());
// ping servers
// ping servers
m_model->queryServersStatus();
}

View File

@ -166,12 +166,9 @@ void WorldListPage::retranslate()
bool WorldListPage::worldListFilter(QKeyEvent* keyEvent)
{
switch (keyEvent->key()) {
case Qt::Key_Delete:
on_actionRemove_triggered();
return true;
default:
break;
if (keyEvent->key() == Qt::Key_Delete) {
on_actionRemove_triggered();
return true;
}
return QWidget::eventFilter(ui->worldTreeView, keyEvent);
}

View File

@ -45,7 +45,9 @@
#include "net/ApiDownload.h"
AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods)
AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent,
const ATLauncher::PackVersion& version,
QVector<ATLauncher::VersionMod> mods)
: QAbstractListModel(parent), m_version(version), m_mods(mods)
{
// fill mod index
@ -233,7 +235,7 @@ void AtlOptionalModListModel::clearAll()
emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn));
}
void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index)
void AtlOptionalModListModel::toggleMod(const ATLauncher::VersionMod& mod, int index)
{
auto enable = !m_selection[mod.name];
@ -251,7 +253,7 @@ void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index)
setMod(mod, index, enable);
}
void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit)
void AtlOptionalModListModel::setMod(const ATLauncher::VersionMod& mod, int index, bool enable, bool shouldEmit)
{
if (m_selection[mod.name] == enable)
return;
@ -313,7 +315,7 @@ void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool
}
}
AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods)
AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, const ATLauncher::PackVersion& version, QVector<ATLauncher::VersionMod> mods)
: QDialog(parent), ui(new Ui::AtlOptionalModDialog)
{
ui->setupUi(this);

View File

@ -55,7 +55,7 @@ class AtlOptionalModListModel : public QAbstractListModel {
DescriptionColumn,
};
AtlOptionalModListModel(QWidget* parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods);
AtlOptionalModListModel(QWidget* parent, const ATLauncher::PackVersion& version, QVector<ATLauncher::VersionMod> mods);
QVector<QString> getResult();
@ -78,8 +78,8 @@ class AtlOptionalModListModel : public QAbstractListModel {
void clearAll();
private:
void toggleMod(ATLauncher::VersionMod mod, int index);
void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true);
void toggleMod(const ATLauncher::VersionMod& mod, int index);
void setMod(const ATLauncher::VersionMod& mod, int index, bool enable, bool shouldEmit = true);
private:
NetJob::Ptr m_jobPtr;
@ -97,7 +97,7 @@ class AtlOptionalModDialog : public QDialog {
Q_OBJECT
public:
AtlOptionalModDialog(QWidget* parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods);
AtlOptionalModDialog(QWidget* parent, const ATLauncher::PackVersion& version, QVector<ATLauncher::VersionMod> mods);
~AtlOptionalModDialog() override;
QVector<QString> getResult() { return listModel->getResult(); }

View File

@ -41,7 +41,7 @@
AtlUserInteractionSupportImpl::AtlUserInteractionSupportImpl(QWidget* parent) : m_parent(parent) {}
std::optional<QVector<QString>> AtlUserInteractionSupportImpl::chooseOptionalMods(ATLauncher::PackVersion version,
std::optional<QVector<QString>> AtlUserInteractionSupportImpl::chooseOptionalMods(const ATLauncher::PackVersion& version,
QVector<ATLauncher::VersionMod> mods)
{
AtlOptionalModDialog optionalModDialog(m_parent, version, mods);

View File

@ -48,7 +48,8 @@ class AtlUserInteractionSupportImpl : public QObject, public ATLauncher::UserInt
private:
QString chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) override;
std::optional<QVector<QString>> chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) override;
std::optional<QVector<QString>> chooseOptionalMods(const ATLauncher::PackVersion& version,
QVector<ATLauncher::VersionMod> mods) override;
void displayMessage(QString message) override;
private:

View File

@ -32,7 +32,7 @@ void FlameModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject&
void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
FlameMod::loadIndexedPackVersions(m, arr);
}
auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion
@ -65,7 +65,7 @@ void FlameResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJso
void FlameResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
FlameMod::loadIndexedPackVersions(m, arr);
}
bool FlameResourcePackModel::optedOut(const ModPlatform::IndexedVersion& ver) const
@ -93,7 +93,7 @@ void FlameTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJson
void FlameTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
FlameMod::loadIndexedPackVersions(m, arr);
QVector<ModPlatform::IndexedVersion> filtered_versions(m.versions.size());
@ -157,7 +157,7 @@ void FlameShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonO
void FlameShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
FlameMod::loadIndexedPackVersions(m, arr);
}
bool FlameShaderPackModel::optedOut(const ModPlatform::IndexedVersion& ver) const

View File

@ -106,9 +106,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const
}
auto pack = m_modpacks.at(pos);
if (role == Qt::ToolTipRole) {
}
switch (role) {
case Qt::ToolTipRole:
return tr("Minecraft %1").arg(pack.mcVersion);

View File

@ -213,7 +213,7 @@ void ListModel::fill(ModpackList modpacks_)
endResetModel();
}
void ListModel::addPack(Modpack modpack)
void ListModel::addPack(const Modpack& modpack)
{
beginResetModel();
this->modpacks.append(modpack);

View File

@ -62,7 +62,7 @@ class ListModel : public QAbstractListModel {
Qt::ItemFlags flags(const QModelIndex& index) const override;
void fill(ModpackList modpacks);
void addPack(Modpack modpack);
void addPack(const Modpack& modpack);
void clear();
void remove(int row);

View File

@ -213,7 +213,7 @@ void Page::ftbPackDataDownloadAborted()
CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)->show();
}
void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack)
void Page::ftbPrivatePackDataDownloadSuccessfully(const Modpack& pack)
{
privateListModel->addPack(pack);
}

View File

@ -85,7 +85,7 @@ class Page : public QWidget, public ModpackProviderBasePage {
void ftbPackDataDownloadFailed(QString reason);
void ftbPackDataDownloadAborted();
void ftbPrivatePackDataDownloadSuccessfully(Modpack pack);
void ftbPrivatePackDataDownloadSuccessfully(const Modpack& pack);
void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode);
void onSortingSelectionChanged(QString data);

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