Merge branch 'PrismLauncher:develop' into data-packs

This commit is contained in:
TheKodeToad 2025-06-01 07:54:16 +00:00 committed by GitHub
commit e4ed3b4546
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
125 changed files with 3925 additions and 1596 deletions

View file

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

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

@ -0,0 +1,124 @@
name: Package for Linux
description: Create Linux packages for Prism Launcher
inputs:
version:
description: Launcher version
required: true
build-type:
description: Type for the build
required: true
default: Debug
artifact-name:
description: Name of the uploaded artifact
required: true
default: Linux
cmake-preset:
description: Base CMake preset previously used for the build
required: true
default: linux
qt-version:
description: Version of Qt to use
required: true
gpg-private-key:
description: Private key for AppImage signing
required: false
gpg-private-key-id:
description: ID for the gpg-private-key, to select the signing key
required: false
runs:
using: composite
steps:
- name: Package AppImage
shell: bash
env:
VERSION: ${{ inputs.version }}
BUILD_DIR: build
INSTALL_APPIMAGE_DIR: install-appdir
GPG_PRIVATE_KEY: ${{ inputs.gpg-private-key }}
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr
mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml
export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated
export OUTPUT="PrismLauncher-Linux-x86_64.AppImage"
chmod +x linuxdeploy-*.AppImage
mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib
mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
cp -r ${{ runner.workspace }}/Qt/${{ inputs.qt-version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
export LD_LIBRARY_PATH
chmod +x AppImageUpdate-x86_64.AppImage
cp AppImageUpdate-x86_64.AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin
export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-x86_64.AppImage.zsync"
if [ '${{ inputs.gpg-private-key-id }}' != '' ]; then
export SIGN=1
export SIGN_KEY=${{ inputs.gpg-private-key-id }}
mkdir -p ~/.gnupg/
echo "$GPG_PRIVATE_KEY" > ~/.gnupg/private.key
gpg --import ~/.gnupg/private.key
else
echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY
fi
./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.prismlauncher.PrismLauncher.svg
mv "PrismLauncher-Linux-x86_64.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build-type }}-x86_64.AppImage"
- name: Package portable tarball
shell: bash
env:
BUILD_DIR: build
CMAKE_PRESET: ${{ inputs.cmake-preset }}
INSTALL_PORTABLE_DIR: install-portable
run: |
cmake --preset "$CMAKE_PRESET" -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_PORTABLE_DIR }} -DINSTALL_BUNDLE=full
cmake --install ${{ env.BUILD_DIR }}
cmake --install ${{ env.BUILD_DIR }} --component portable
mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libffi.so.*.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
cd ${{ env.INSTALL_PORTABLE_DIR }}
tar -czf ../PrismLauncher-portable.tar.gz *
- name: Upload binary tarball
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ inputs.artifact-name }}-Qt6-Portable-${{ inputs.version }}-${{ inputs.build-type }}
path: PrismLauncher-portable.tar.gz
- name: Upload AppImage
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage
path: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage
- name: Upload AppImage Zsync
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage.zsync
path: PrismLauncher-Linux-x86_64.AppImage.zsync

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

@ -0,0 +1,121 @@
name: Package for macOS
description: Create a macOS package for Prism Launcher
inputs:
version:
description: Launcher version
required: true
build-type:
description: Type for the build
required: true
default: Debug
artifact-name:
description: Name of the uploaded artifact
required: true
default: macOS
apple-codesign-cert:
description: Certificate for signing macOS builds
required: false
apple-codesign-password:
description: Password for signing macOS builds
required: false
apple-codesign-id:
description: Certificate ID for signing macOS builds
required: false
apple-notarize-apple-id:
description: Apple ID used for notarizing macOS builds
required: false
apple-notarize-team-id:
description: Team ID used for notarizing macOS builds
required: false
apple-notarize-password:
description: Password used for notarizing macOS builds
required: false
sparkle-ed25519-key:
description: Private key for signing Sparkle updates
required: false
runs:
using: composite
steps:
- name: Fetch codesign certificate
shell: bash
run: |
echo '${{ inputs.apple-codesign-cert }}' | base64 --decode > codesign.p12
if [ -n '${{ inputs.apple-codesign-id }}' ]; then
security create-keychain -p '${{ inputs.apple-codesign-password }}' build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p '${{ inputs.apple-codesign-password }}' build.keychain
security import codesign.p12 -k build.keychain -P '${{ inputs.apple-codesign-password }}' -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k '${{ inputs.apple-codesign-password }}' build.keychain
else
echo ":warning: Using ad-hoc code signing for macOS, as certificate was not present." >> $GITHUB_STEP_SUMMARY
fi
- name: Package
shell: bash
env:
BUILD_DIR: build
INSTALL_DIR: install
run: |
cmake --install ${{ env.BUILD_DIR }}
cd ${{ env.INSTALL_DIR }}
chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher"
if [ -n '${{ inputs.apple-codesign-id }}' ]; then
APPLE_CODESIGN_ID='${{ inputs.apple-codesign-id }}'
ENTITLEMENTS_FILE='../program_info/App.entitlements'
else
APPLE_CODESIGN_ID='-'
ENTITLEMENTS_FILE='../program_info/AdhocSignedApp.entitlements'
fi
sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "$ENTITLEMENTS_FILE" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
mv "PrismLauncher.app" "Prism Launcher.app"
- name: Notarize
shell: bash
env:
INSTALL_DIR: install
run: |
cd ${{ env.INSTALL_DIR }}
if [ -n '${{ inputs.apple-notarize-password }}' ]; then
ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip
xcrun notarytool submit ../PrismLauncher.zip \
--wait --progress \
--apple-id '${{ inputs.apple-notarize-apple-id }}' \
--team-id '${{ inputs.apple-notarize-team-id }}' \
--password '${{ inputs.apple-notarize-password }}'
xcrun stapler staple "Prism Launcher.app"
else
echo ":warning: Skipping notarization as credentials are not present." >> $GITHUB_STEP_SUMMARY
fi
ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip
- name: Make Sparkle signature
shell: bash
run: |
if [ '${{ inputs.sparkle-ed25519-key }}' != '' ]; then
echo '${{ inputs.sparkle-ed25519-key }}' > ed25519-priv.pem
signature=$(/opt/homebrew/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n)
rm ed25519-priv.pem
cat >> $GITHUB_STEP_SUMMARY << EOF
### Artifact Information :information_source:
- :memo: Sparkle Signature (ed25519): \`$signature\`
EOF
else
cat >> $GITHUB_STEP_SUMMARY << EOF
### Artifact Information :information_source:
- :warning: Sparkle Signature (ed25519): No private key available (likely a pull request or fork)
EOF
fi
- name: Upload binary tarball
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}
path: PrismLauncher.zip

View file

@ -0,0 +1,143 @@
name: Package for Windows
description: Create a Windows package for Prism Launcher
inputs:
version:
description: Launcher version
required: true
build-type:
description: Type for the build
required: true
default: Debug
artifact-name:
description: Name of the uploaded artifact
required: true
msystem:
description: MSYS2 subsystem to use
required: true
default: false
windows-codesign-cert:
description: Certificate for signing Windows builds
required: false
windows-codesign-password:
description: Password for signing Windows builds
required: false
runs:
using: composite
steps:
- name: Package (MinGW)
if: ${{ inputs.msystem != '' }}
shell: msys2 {0}
env:
BUILD_DIR: build
INSTALL_DIR: install
run: |
cmake --install ${{ env.BUILD_DIR }}
touch ${{ env.INSTALL_DIR }}/manifest.txt
for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt
- name: Package (MSVC)
if: ${{ inputs.msystem == '' }}
shell: pwsh
env:
BUILD_DIR: build
INSTALL_DIR: install
run: |
cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }}
cd ${{ github.workspace }}
Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
- name: Fetch codesign certificate
shell: bash # yes, we are not using MSYS2 or PowerShell here
run: |
echo '${{ inputs.windows-codesign-cert }}' | base64 --decode > codesign.pfx
- name: Sign executable
shell: pwsh
env:
INSTALL_DIR: install
run: |
if (Get-Content ./codesign.pfx){
cd ${{ env.INSTALL_DIR }}
# We ship the exact same executable for portable and non-portable editions, so signing just once is fine
SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ inputs.windows-codesign-password }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_updater.exe prismlauncher_filelink.exe
} else {
":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
}
- name: Package (MinGW, portable)
if: ${{ inputs.msystem != '' }}
shell: msys2 {0}
env:
BUILD_DIR: build
INSTALL_DIR: install
INSTALL_PORTABLE_DIR: install-portable
run: |
cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
- name: Package (MSVC, portable)
if: ${{ inputs.msystem == '' }}
shell: pwsh
env:
BUILD_DIR: build
INSTALL_DIR: install
INSTALL_PORTABLE_DIR: install-portable
run: |
cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt
- name: Package (installer)
shell: pwsh
env:
BUILD_DIR: build
INSTALL_DIR: install
NSCURL_VERSION: "v24.9.26.122"
NSCURL_SHA256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
run: |
New-Item -Name NSISPlugins -ItemType Directory
Invoke-Webrequest https://github.com/negrutiu/nsis-nscurl/releases/download/"${{ env.NSCURL_VERSION }}"/NScurl.zip -OutFile NSISPlugins\NScurl.zip
$nscurl_hash = Get-FileHash NSISPlugins\NScurl.zip -Algorithm Sha256 | Select-Object -ExpandProperty Hash
if ( $nscurl_hash -ne "${{ env.nscurl_sha256 }}") {
echo "::error:: NSCurl.zip sha256 mismatch"
exit 1
}
Expand-Archive -Path NSISPlugins\NScurl.zip -DestinationPath NSISPlugins\NScurl
cd ${{ env.INSTALL_DIR }}
makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi"
- name: Sign installer
shell: pwsh
run: |
if (Get-Content ./codesign.pfx){
SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ inputs.windows-codesign-password }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe
} else {
":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY
}
- name: Upload binary zip
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}
path: install/**
- name: Upload portable zip
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ inputs.artifact-name }}-Portable-${{ inputs.version }}-${{ inputs.build-type }}
path: install-portable/**
- name: Upload installer
uses: actions/upload-artifact@v4
with:
name: PrismLauncher-${{ inputs.artifact-name }}-Setup-${{ inputs.version }}-${{ inputs.build-type }}
path: PrismLauncher-Setup.exe

View file

@ -0,0 +1,78 @@
name: Setup Dependencies
description: Install and setup dependencies for building Prism Launcher
inputs:
build-type:
description: Type for the build
required: true
default: Debug
msystem:
description: MSYS2 subsystem to use
required: false
vcvars-arch:
description: Visual Studio architecture to use
required: false
qt-architecture:
description: Qt architecture
required: false
qt-version:
description: Version of Qt to use
required: true
default: 6.8.1
outputs:
build-type:
description: Type of build used
value: ${{ inputs.build-type }}
qt-version:
description: Version of Qt used
value: ${{ inputs.qt-version }}
runs:
using: composite
steps:
- name: Setup Linux dependencies
if: ${{ runner.os == 'Linux' }}
uses: ./.github/actions/setup-dependencies/linux
- name: Setup macOS dependencies
if: ${{ runner.os == 'macOS' }}
uses: ./.github/actions/setup-dependencies/macos
- name: Setup Windows dependencies
if: ${{ runner.os == 'Windows' }}
uses: ./.github/actions/setup-dependencies/windows
with:
build-type: ${{ inputs.build-type }}
msystem: ${{ inputs.msystem }}
vcvars-arch: ${{ inputs.vcvars-arch }}
# TODO(@getchoo): Get this working on MSYS2!
- name: Setup ccache
if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }}
uses: hendrikmuhs/ccache-action@v1.2.18
with:
variant: ${{ runner.os == 'Windows' && 'sccache' || 'ccache' }}
create-symlink: ${{ runner.os != 'Windows' }}
key: ${{ runner.os }}-qt${{ inputs.qt_ver }}-${{ inputs.architecture }}
- name: Use ccache on debug builds
if: ${{ inputs.build-type == 'Debug' }}
shell: bash
env:
# Only use sccache on MSVC
CCACHE_VARIANT: ${{ (runner.os == 'Windows' && inputs.msystem == '') && 'sccache' || 'ccache' }}
run: |
echo "CMAKE_C_COMPILER_LAUNCHER=$CCACHE_VARIANT" >> "$GITHUB_ENV"
echo "CMAKE_CXX_COMPILER_LAUNCHER=$CCACHE_VARIANT" >> "$GITHUB_ENV"
- name: Install Qt
if: ${{ inputs.msystem == '' }}
uses: jurplel/install-qt-action@v4
with:
aqtversion: "==3.1.*"
version: ${{ inputs.qt-version }}
arch: ${{ inputs.qt-architecture }}
modules: qt5compat qtimageformats qtnetworkauth
cache: ${{ inputs.build-type == 'Debug' }}

View file

@ -0,0 +1,26 @@
name: Setup Linux dependencies
runs:
using: composite
steps:
- name: Install host dependencies
shell: bash
run: |
sudo apt-get -y update
sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream libxcb-cursor-dev
- name: Setup AppImage tooling
shell: bash
run: |
declare -A appimage_deps
appimage_deps["https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage"]="4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a linuxdeploy-x86_64.AppImage"
appimage_deps["https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-x86_64.AppImage"]="15106be885c1c48a021198e7e1e9a48ce9d02a86dd0a1848f00bdbf3c1c92724 linuxdeploy-plugin-qt-x86_64.AppImage"
appimage_deps["https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-x86_64.AppImage"]="f1747cf60058e99f1bb9099ee9787d16c10241313b7acec81810ea1b1e568c11 AppImageUpdate-x86_64.AppImage"
for url in "${!appimage_deps[@]}"; do
curl -LO "$url"
sha256sum -c - <<< "${appimage_deps[$url]}"
done
sudo apt -y install libopengl0

View file

@ -0,0 +1,16 @@
name: Setup macOS dependencies
runs:
using: composite
steps:
- name: Install dependencies
shell: bash
run: |
brew update
brew install ninja extra-cmake-modules temurin@17
- name: Set JAVA_HOME
shell: bash
run: |
echo "JAVA_HOME=$(/usr/libexec/java_home -v 17)" >> "$GITHUB_ENV"

View file

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

View file

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

View file

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

View file

@ -2,37 +2,50 @@ name: "CodeQL Code Scanning"
on: on:
push: push:
# NOTE: `!` doesn't work with `paths-ignore` :(
# So we a catch-all glob instead
# https://github.com/orgs/community/discussions/25369#discussioncomment-3247674
paths: paths:
- "**" # File types
- "!.github/**" - "**.cpp"
- ".github/workflows/codeql.yml" - "**.h"
- "!flatpak/" - "**.java"
- "!nix/"
- "!scripts/"
- "!.git*" # Directories
- "!.envrc" - "buildconfig/"
- "!**.md" - "cmake/"
- "launcher/"
- "libraries/"
- "program_info/"
- "tests/"
# Files
- "CMakeLists.txt"
- "COPYING.md" - "COPYING.md"
- "!renovate.json"
# Workflows
- ".github/codeql"
- ".github/workflows/codeql.yml"
- ".github/actions/setup-dependencies/"
pull_request: pull_request:
# See above
paths: paths:
- "**" # File types
- "!.github/**" - "**.cpp"
- ".github/workflows/codeql.yml" - "**.h"
- "!flatpak/"
- "!nix/"
- "!scripts/"
- "!.git*" # Directories
- "!.envrc" - "buildconfig/"
- "!**.md" - "cmake/"
- "launcher/"
- "libraries/"
- "program_info/"
- "tests/"
# Files
- "CMakeLists.txt"
- "COPYING.md" - "COPYING.md"
- "!renovate.json"
# Workflows
- ".github/codeql"
- ".github/workflows/codeql.yml"
- ".github/actions/setup-dependencies/"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@ -52,28 +65,15 @@ jobs:
queries: security-and-quality queries: security-and-quality
languages: cpp, java languages: cpp, java
- name: Install Dependencies - name: Setup dependencies
run: sudo apt-get -y update uses: ./.github/actions/setup-dependencies
sudo apt-get -y install ninja-build extra-cmake-modules scdoc
- name: Install Qt
uses: jurplel/install-qt-action@v3
with: with:
aqtversion: "==3.1.*" build-type: Debug
py7zrversion: ">=0.20.2"
version: "6.8.1"
host: "linux"
target: "desktop"
arch: ""
modules: "qt5compat qtimageformats qtnetworkauth"
tools: ""
- name: Configure and Build - name: Configure and Build
run: | run: |
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr -G Ninja cmake --preset linux_debug
cmake --build --preset linux_debug
cmake --build build
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v3

View file

@ -5,35 +5,52 @@ on:
# We don't do anything with these artifacts on releases. They go to Flathub # We don't do anything with these artifacts on releases. They go to Flathub
tags-ignore: tags-ignore:
- "*" - "*"
# NOTE: `!` doesn't work with `paths-ignore` :(
# So we a catch-all glob instead
# https://github.com/orgs/community/discussions/25369#discussioncomment-3247674
paths: paths:
- "**" # File types
- "!.github/**" - "**.cpp"
- ".github/workflows/flatpak.yml" - "**.h"
- "!nix/" - "**.java"
- "!scripts/"
- "!.git*" # Build files
- "!.envrc" - "flatpak/"
- "!**.md"
# Directories
- "buildconfig/"
- "cmake/"
- "launcher/"
- "libraries/"
- "program_info/"
- "tests/"
# Files
- "CMakeLists.txt"
- "COPYING.md" - "COPYING.md"
- "!renovate.json"
# Workflows
- ".github/workflows/flatpak.yml"
pull_request: pull_request:
# See above
paths: paths:
- "**" # File types
- "!.github/**" - "**.cpp"
- ".github/workflows/flatpak.yml" - "**.h"
- "!nix/"
- "!scripts/"
- "!.git*" # Build files
- "!.envrc" - "flatpak/"
- "!**.md"
# Directories
- "buildconfig/"
- "cmake/"
- "launcher/"
- "libraries/"
- "program_info/"
- "tests/"
# Files
- "CMakeLists.txt"
- "COPYING.md" - "COPYING.md"
- "!renovate.json"
# Workflows
- ".github/workflows/flatpak.yml"
workflow_dispatch: workflow_dispatch:
permissions: permissions:

View file

@ -4,34 +4,56 @@ on:
push: push:
tags: tags:
- "*" - "*"
# NOTE: `!` doesn't work with `paths-ignore` :(
# So we a catch-all glob instead
# https://github.com/orgs/community/discussions/25369#discussioncomment-3247674
paths: paths:
- "**" # File types
- "!.github/**" - "**.cpp"
- ".github/workflows/nix.yml" - "**.h"
- "!flatpak/" - "**.java"
- "!scripts/"
- "!.git*" # Build files
- "!.envrc" - "**.nix"
- "!**.md" - "nix/"
- "flake.lock"
# Directories
- "buildconfig/"
- "cmake/"
- "launcher/"
- "libraries/"
- "program_info/"
- "tests/"
# Files
- "CMakeLists.txt"
- "COPYING.md" - "COPYING.md"
- "!renovate.json"
# Workflows
- ".github/workflows/nix.yml"
pull_request_target: pull_request_target:
paths: paths:
- "**" # File types
- "!.github/**" - "**.cpp"
- ".github/workflows/nix.yml" - "**.h"
- "!flatpak/"
- "!scripts/"
- "!.git*" # Build files
- "!.envrc" - "**.nix"
- "!**.md" - "nix/"
- "flake.lock"
# Directories
- "buildconfig/"
- "cmake/"
- "launcher/"
- "libraries/"
- "program_info/"
- "tests/"
# Files
- "CMakeLists.txt"
- "COPYING.md" - "COPYING.md"
- "!renovate.json"
# Workflows
- ".github/workflows/nix.yml"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -89,7 +111,7 @@ jobs:
# For PRs # For PRs
- name: Setup Nix Magic Cache - name: Setup Nix Magic Cache
if: ${{ env.USE_DETERMINATE == 'true' }} if: ${{ env.USE_DETERMINATE == 'true' }}
uses: DeterminateSystems/flakehub-cache-action@v1 uses: DeterminateSystems/flakehub-cache-action@v2
# For in-tree builds # For in-tree builds
- name: Setup Cachix - name: Setup Cachix

View file

@ -10,20 +10,8 @@ jobs:
name: Build Release name: Build Release
uses: ./.github/workflows/build.yml uses: ./.github/workflows/build.yml
with: with:
build_type: Release build-type: Release
is_qt_cached: false secrets: inherit
secrets:
SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }}
APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }}
APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }}
APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}
create_release: create_release:
needs: build_release needs: build_release
@ -78,6 +66,17 @@ jobs:
cd .. cd ..
done done
for d in PrismLauncher-Windows-MinGW-arm64*; do
cd "${d}" || continue
INST="$(echo -n ${d} | grep -o Setup || true)"
PORT="$(echo -n ${d} | grep -o Portable || true)"
NAME="PrismLauncher-Windows-MinGW-arm64"
test -z "${PORT}" || NAME="${NAME}-Portable"
test -z "${INST}" || mv PrismLauncher-*.exe ../${NAME}-Setup-${{ env.VERSION }}.exe
test -n "${INST}" || zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" *
cd ..
done
- name: Create release - name: Create release
id: create_release id: create_release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
@ -94,6 +93,9 @@ jobs:
PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip
PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MinGW-w64-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MinGW-w64-Setup-${{ env.VERSION }}.exe
PrismLauncher-Windows-MinGW-arm64-${{ env.VERSION }}.zip
PrismLauncher-Windows-MinGW-arm64-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MinGW-arm64-Setup-${{ env.VERSION }}.exe
PrismLauncher-Windows-MSVC-arm64-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-arm64-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-arm64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-arm64-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-arm64-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MSVC-arm64-Setup-${{ env.VERSION }}.exe

View file

@ -1,60 +0,0 @@
name: Build Application
on:
push:
branches-ignore:
- "renovate/**"
# NOTE: `!` doesn't work with `paths-ignore` :(
# So we a catch-all glob instead
# https://github.com/orgs/community/discussions/25369#discussioncomment-3247674
paths:
- "**"
- "!.github/**"
- ".github/workflows/build.yml"
- ".github/workflows/trigger_builds.yml"
- "!flatpak/"
- "!nix/"
- "!scripts/"
- "!.git*"
- "!.envrc"
- "!**.md"
- "COPYING.md"
- "!renovate.json"
pull_request:
# See above
paths:
- "**"
- "!.github/**"
- ".github/workflows/build.yml"
- ".github/workflows/trigger_builds.yml"
- "!flatpak/"
- "!nix/"
- "!scripts/"
- "!.git*"
- "!.envrc"
- "!**.md"
- "COPYING.md"
- "!renovate.json"
workflow_dispatch:
jobs:
build_debug:
name: Build Debug
uses: ./.github/workflows/build.yml
with:
build_type: Debug
is_qt_cached: true
secrets:
SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }}
APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }}
APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }}
APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}

View file

@ -17,9 +17,9 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: cachix/install-nix-action@754537aaedb35f72ab11a60cc162c49ef3016495 # v31 - uses: cachix/install-nix-action@17fe5fb4a23ad6cbbe47d6b3f359611ad276644c # v31
- uses: DeterminateSystems/update-flake-lock@v24 - uses: DeterminateSystems/update-flake-lock@v25
with: with:
commit-msg: "chore(nix): update lockfile" commit-msg: "chore(nix): update lockfile"
pr-title: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile"

1
.gitignore vendored
View file

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

4
.gitmodules vendored
View file

@ -19,6 +19,6 @@
[submodule "flatpak/shared-modules"] [submodule "flatpak/shared-modules"]
path = flatpak/shared-modules path = flatpak/shared-modules
url = https://github.com/flathub/shared-modules.git url = https://github.com/flathub/shared-modules.git
[submodule "libraries/qt-qrcodegenerator/QR-Code-generator"] [submodule "libraries/qrcodegenerator"]
path = libraries/qt-qrcodegenerator/QR-Code-generator path = libraries/qrcodegenerator
url = https://github.com/nayuki/QR-Code-generator url = https://github.com/nayuki/QR-Code-generator

View file

@ -475,7 +475,6 @@ add_subdirectory(libraries/libnbtplusplus)
add_subdirectory(libraries/systeminfo) # system information library add_subdirectory(libraries/systeminfo) # system information library
add_subdirectory(libraries/launcher) # java based launcher part for Minecraft add_subdirectory(libraries/launcher) # java based launcher part for Minecraft
add_subdirectory(libraries/javacheck) # java compatibility checker add_subdirectory(libraries/javacheck) # java compatibility checker
add_subdirectory(libraries/qt-qrcodegenerator) # qr code generator
if(FORCE_BUNDLED_ZLIB) if(FORCE_BUNDLED_ZLIB)
message(STATUS "Using bundled zlib") message(STATUS "Using bundled zlib")
@ -533,6 +532,15 @@ add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
add_subdirectory(libraries/qdcss) # css parser add_subdirectory(libraries/qdcss) # css parser
# qr code generator
set(QRCODE_SOURCES
libraries/qrcodegenerator/cpp/qrcodegen.cpp
libraries/qrcodegenerator/cpp/qrcodegen.hpp
)
add_library(qrcodegenerator STATIC ${QRCODE_SOURCES})
target_include_directories(qrcodegenerator PUBLIC "libraries/qrcodegenerator/cpp/" )
generate_export_header(qrcodegenerator)
############################### Built Artifacts ############################### ############################### Built Artifacts ###############################
add_subdirectory(buildconfig) add_subdirectory(buildconfig)

14
CMakePresets.json Normal file
View file

@ -0,0 +1,14 @@
{
"$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
"version": 8,
"cmakeMinimumRequired": {
"major": 3,
"minor": 28
},
"include": [
"cmake/linuxPreset.json",
"cmake/macosPreset.json",
"cmake/windowsMinGWPreset.json",
"cmake/windowsMSVCPreset.json"
]
}

View file

@ -404,7 +404,7 @@
You should have received a copy of the GNU Lesser General Public You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>. License along with this library. If not, see <http://www.gnu.org/licenses/>.
## qt-qrcodegenerator (`libraries/qt-qrcodegenerator`) ## QR-Code-generator (`libraries/qrcodegenerator`)
Copyright © 2024 Project Nayuki. (MIT License) Copyright © 2024 Project Nayuki. (MIT License)
https://www.nayuki.io/page/qr-code-generator-library https://www.nayuki.io/page/qr-code-generator-library

View file

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

View file

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

View file

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

81
cmake/commonPresets.json Normal file
View file

@ -0,0 +1,81 @@
{
"$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
"version": 8,
"configurePresets": [
{
"name": "base",
"hidden": true,
"binaryDir": "build",
"installDir": "install",
"cacheVariables": {
"Launcher_BUILD_PLATFORM": "custom"
}
},
{
"name": "base_debug",
"hidden": true,
"inherits": [
"base"
],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "base_release",
"hidden": true,
"inherits": [
"base"
],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"ENABLE_LTO": "ON"
}
},
{
"name": "base_ci",
"hidden": true,
"inherits": [
"base_release"
],
"cacheVariables": {
"Launcher_BUILD_PLATFORM": "official",
"Launcher_FORCE_BUNDLED_LIBS": "ON"
}
}
],
"testPresets": [
{
"name": "base",
"hidden": true,
"output": {
"outputOnFailure": true
},
"execution": {
"noTestsAction": "error"
},
"filter": {
"exclude": {
"name": "^example64|example$"
}
}
},
{
"name": "base_debug",
"hidden": true,
"inherits": [
"base"
],
"output": {
"debug": true
}
},
{
"name": "base_release",
"hidden": true,
"inherits": [
"base"
]
}
]
}

180
cmake/linuxPreset.json Normal file
View file

@ -0,0 +1,180 @@
{
"$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
"version": 8,
"include": [
"commonPresets.json"
],
"configurePresets": [
{
"name": "linux_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
},
"generator": "Ninja",
"cacheVariables": {
"Launcher_BUILD_ARTIFACT": "Linux-Qt6",
"Launcher_ENABLE_JAVA_DOWNLOADER": "ON"
}
},
{
"name": "linux_debug",
"inherits": [
"base_debug",
"linux_base"
],
"displayName": "Linux (Debug)"
},
{
"name": "linux_release",
"inherits": [
"base_release",
"linux_base"
],
"displayName": "Linux (Release)"
},
{
"name": "linux_ci",
"inherits": [
"base_ci",
"linux_base"
],
"displayName": "Linux (CI)",
"cacheVariables": {
"Launcher_BUILD_ARTIFACT": "Linux-Qt6"
},
"installDir": "/usr"
}
],
"buildPresets": [
{
"name": "linux_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
}
},
{
"name": "linux_debug",
"inherits": [
"linux_base"
],
"displayName": "Linux (Debug)",
"configurePreset": "linux_debug"
},
{
"name": "linux_release",
"inherits": [
"linux_base"
],
"displayName": "Linux (Release)",
"configurePreset": "linux_release"
},
{
"name": "linux_ci",
"inherits": [
"linux_base"
],
"displayName": "Linux (CI)",
"configurePreset": "linux_ci"
}
],
"testPresets": [
{
"name": "linux_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
}
},
{
"name": "linux_debug",
"inherits": [
"base_debug",
"linux_base"
],
"displayName": "Linux (Debug)",
"configurePreset": "linux_debug"
},
{
"name": "linux_release",
"inherits": [
"base_release",
"linux_base"
],
"displayName": "Linux (Release)",
"configurePreset": "linux_release"
},
{
"name": "linux_ci",
"inherits": [
"base_release",
"linux_base"
],
"displayName": "Linux (CI)",
"configurePreset": "linux_ci"
}
],
"workflowPresets": [
{
"name": "linux_debug",
"displayName": "Linux (Debug)",
"steps": [
{
"type": "configure",
"name": "linux_debug"
},
{
"type": "build",
"name": "linux_debug"
},
{
"type": "test",
"name": "linux_debug"
}
]
},
{
"name": "linux",
"displayName": "Linux (Release)",
"steps": [
{
"type": "configure",
"name": "linux_release"
},
{
"type": "build",
"name": "linux_release"
},
{
"type": "test",
"name": "linux_release"
}
]
},
{
"name": "linux_ci",
"displayName": "Linux (CI)",
"steps": [
{
"type": "configure",
"name": "linux_ci"
},
{
"type": "build",
"name": "linux_ci"
},
{
"type": "test",
"name": "linux_ci"
}
]
}
]
}

272
cmake/macosPreset.json Normal file
View file

@ -0,0 +1,272 @@
{
"$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
"version": 8,
"include": [
"commonPresets.json"
],
"configurePresets": [
{
"name": "macos_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
},
"generator": "Ninja"
},
{
"name": "macos_universal_base",
"hidden": true,
"inherits": [
"macos_base"
],
"cacheVariables": {
"CMAKE_OSX_ARCHITECTURES": "x86_64;arm64",
"Launcher_BUILD_ARTIFACT": "macOS-Qt6"
}
},
{
"name": "macos_debug",
"inherits": [
"base_debug",
"macos_base"
],
"displayName": "macOS (Debug)"
},
{
"name": "macos_release",
"inherits": [
"base_release",
"macos_base"
],
"displayName": "macOS (Release)"
},
{
"name": "macos_universal_debug",
"inherits": [
"base_debug",
"macos_universal_base"
],
"displayName": "macOS (Universal Binary, Debug)"
},
{
"name": "macos_universal_release",
"inherits": [
"base_release",
"macos_universal_base"
],
"displayName": "macOS (Universal Binary, Release)"
},
{
"name": "macos_ci",
"inherits": [
"base_ci",
"macos_universal_base"
],
"displayName": "macOS (CI)",
"cacheVariables": {
"Launcher_BUILD_ARTIFACT": "macOS-Qt6"
}
}
],
"buildPresets": [
{
"name": "macos_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
}
},
{
"name": "macos_debug",
"inherits": [
"macos_base"
],
"displayName": "macOS (Debug)",
"configurePreset": "macos_debug"
},
{
"name": "macos_release",
"inherits": [
"macos_base"
],
"displayName": "macOS (Release)",
"configurePreset": "macos_release"
},
{
"name": "macos_universal_debug",
"inherits": [
"macos_base"
],
"displayName": "macOS (Universal Binary, Debug)",
"configurePreset": "macos_universal_debug"
},
{
"name": "macos_universal_release",
"inherits": [
"macos_base"
],
"displayName": "macOS (Universal Binary, Release)",
"configurePreset": "macos_universal_release"
},
{
"name": "macos_ci",
"inherits": [
"macos_base"
],
"displayName": "macOS (CI)",
"configurePreset": "macos_ci"
}
],
"testPresets": [
{
"name": "macos_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
}
},
{
"name": "macos_debug",
"inherits": [
"base_debug",
"macos_base"
],
"displayName": "MacOS (Debug)",
"configurePreset": "macos_debug"
},
{
"name": "macos_release",
"inherits": [
"base_release",
"macos_base"
],
"displayName": "macOS (Release)",
"configurePreset": "macos_release"
},
{
"name": "macos_universal_debug",
"inherits": [
"base_debug",
"macos_base"
],
"displayName": "MacOS (Universal Binary, Debug)",
"configurePreset": "macos_universal_debug"
},
{
"name": "macos_universal_release",
"inherits": [
"base_release",
"macos_base"
],
"displayName": "macOS (Universal Binary, Release)",
"configurePreset": "macos_universal_release"
},
{
"name": "macos_ci",
"inherits": [
"base_release",
"macos_base"
],
"displayName": "macOS (CI)",
"configurePreset": "macos_ci"
}
],
"workflowPresets": [
{
"name": "macos_debug",
"displayName": "macOS (Debug)",
"steps": [
{
"type": "configure",
"name": "macos_debug"
},
{
"type": "build",
"name": "macos_debug"
},
{
"type": "test",
"name": "macos_debug"
}
]
},
{
"name": "macos",
"displayName": "macOS (Release)",
"steps": [
{
"type": "configure",
"name": "macos_release"
},
{
"type": "build",
"name": "macos_release"
},
{
"type": "test",
"name": "macos_release"
}
]
},
{
"name": "macos_universal_debug",
"displayName": "macOS (Universal Binary, Debug)",
"steps": [
{
"type": "configure",
"name": "macos_universal_debug"
},
{
"type": "build",
"name": "macos_universal_debug"
},
{
"type": "test",
"name": "macos_universal_debug"
}
]
},
{
"name": "macos_universal",
"displayName": "macOS (Universal Binary, Release)",
"steps": [
{
"type": "configure",
"name": "macos_universal_release"
},
{
"type": "build",
"name": "macos_universal_release"
},
{
"type": "test",
"name": "macos_universal_release"
}
]
},
{
"name": "macos_ci",
"displayName": "macOS (CI)",
"steps": [
{
"type": "configure",
"name": "macos_ci"
},
{
"type": "build",
"name": "macos_ci"
},
{
"type": "test",
"name": "macos_ci"
}
]
}
]
}

View file

@ -0,0 +1,311 @@
{
"$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
"version": 8,
"include": [
"commonPresets.json"
],
"configurePresets": [
{
"name": "windows_msvc_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"cacheVariables": {
"Launcher_BUILD_ARTIFACT": "Windows-MSVC-Qt6"
}
},
{
"name": "windows_msvc_arm64_cross_base",
"hidden": true,
"inherits": [
"windows_msvc_base"
],
"architecture": "arm64",
"cacheVariables": {
"Launcher_BUILD_ARTIFACT": "Windows-MSVC-arm64-Qt6"
}
},
{
"name": "windows_msvc_debug",
"inherits": [
"base_debug",
"windows_msvc_base"
],
"displayName": "Windows MSVC (Debug)",
"generator": "Ninja"
},
{
"name": "windows_msvc_release",
"inherits": [
"base_release",
"windows_msvc_base"
],
"displayName": "Windows MSVC (Release)"
},
{
"name": "windows_msvc_arm64_cross_debug",
"inherits": [
"base_debug",
"windows_msvc_arm64_cross_base"
],
"displayName": "Windows MSVC (ARM64 cross, Debug)"
},
{
"name": "windows_msvc_arm64_cross_release",
"inherits": [
"base_release",
"windows_msvc_arm64_cross_base"
],
"displayName": "Windows MSVC (ARM64 cross, Release)"
},
{
"name": "windows_msvc_ci",
"inherits": [
"base_ci",
"windows_msvc_base"
],
"displayName": "Windows MSVC (CI)",
"cacheVariables": {
"Launcher_BUILD_ARTIFACT": "Windows-MSVC-Qt6"
}
},
{
"name": "windows_msvc_arm64_cross_ci",
"inherits": [
"base_ci",
"windows_msvc_arm64_cross_base"
],
"displayName": "Windows MSVC (ARM64 cross, CI)",
"cacheVariables": {
"Launcher_BUILD_ARTIFACT": "Windows-MSVC-arm64-Qt6"
}
}
],
"buildPresets": [
{
"name": "windows_msvc_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "windows_msvc_debug",
"inherits": [
"windows_msvc_base"
],
"displayName": "Windows MSVC (Debug)",
"configurePreset": "windows_msvc_debug",
"configuration": "Debug"
},
{
"name": "windows_msvc_release",
"inherits": [
"windows_msvc_base"
],
"displayName": "Windows MSVC (Release)",
"configurePreset": "windows_msvc_release",
"configuration": "Release",
"nativeToolOptions": [
"/p:UseMultiToolTask=true",
"/p:EnforceProcessCountAcrossBuilds=true"
]
},
{
"name": "windows_msvc_arm64_cross_debug",
"inherits": [
"windows_msvc_base"
],
"displayName": "Windows MSVC (ARM64 cross, Debug)",
"configurePreset": "windows_msvc_arm64_cross_debug",
"configuration": "Debug",
"nativeToolOptions": [
"/p:UseMultiToolTask=true",
"/p:EnforceProcessCountAcrossBuilds=true"
]
},
{
"name": "windows_msvc_arm64_cross_release",
"inherits": [
"windows_msvc_base"
],
"displayName": "Windows MSVC (ARM64 cross, Release)",
"configurePreset": "windows_msvc_arm64_cross_release",
"configuration": "Release",
"nativeToolOptions": [
"/p:UseMultiToolTask=true",
"/p:EnforceProcessCountAcrossBuilds=true"
]
},
{
"name": "windows_msvc_ci",
"inherits": [
"windows_msvc_base"
],
"displayName": "Windows MSVC (CI)",
"configurePreset": "windows_msvc_ci",
"configuration": "Release",
"nativeToolOptions": [
"/p:UseMultiToolTask=true",
"/p:EnforceProcessCountAcrossBuilds=true"
]
},
{
"name": "windows_msvc_arm64_cross_ci",
"inherits": [
"windows_msvc_base"
],
"displayName": "Windows MSVC (ARM64 cross, CI)",
"configurePreset": "windows_msvc_arm64_cross_ci",
"configuration": "Release",
"nativeToolOptions": [
"/p:UseMultiToolTask=true",
"/p:EnforceProcessCountAcrossBuilds=true"
]
}
],
"testPresets": [
{
"name": "windows_msvc_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "windows_msvc_debug",
"inherits": [
"base_debug",
"windows_msvc_base"
],
"displayName": "Windows MSVC (Debug)",
"configurePreset": "windows_msvc_debug",
"configuration": "Debug"
},
{
"name": "windows_msvc_release",
"inherits": [
"base_release",
"windows_msvc_base"
],
"displayName": "Windows MSVC (Release)",
"configurePreset": "windows_msvc_release",
"configuration": "Release"
},
{
"name": "windows_msvc_ci",
"inherits": [
"base_release",
"windows_msvc_base"
],
"displayName": "Windows MSVC (CI)",
"configurePreset": "windows_msvc_ci",
"configuration": "Release"
}
],
"workflowPresets": [
{
"name": "windows_msvc_debug",
"displayName": "Windows MSVC (Debug)",
"steps": [
{
"type": "configure",
"name": "windows_msvc_debug"
},
{
"type": "build",
"name": "windows_msvc_debug"
},
{
"type": "test",
"name": "windows_msvc_debug"
}
]
},
{
"name": "windows_msvc",
"displayName": "Windows MSVC (Release)",
"steps": [
{
"type": "configure",
"name": "windows_msvc_release"
},
{
"type": "build",
"name": "windows_msvc_release"
},
{
"type": "test",
"name": "windows_msvc_release"
}
]
},
{
"name": "windows_msvc_arm64_cross_debug",
"displayName": "Windows MSVC (ARM64 cross, Debug)",
"steps": [
{
"type": "configure",
"name": "windows_msvc_arm64_cross_debug"
},
{
"type": "build",
"name": "windows_msvc_arm64_cross_debug"
}
]
},
{
"name": "windows_msvc_arm64_cross",
"displayName": "Windows MSVC (ARM64 cross, Release)",
"steps": [
{
"type": "configure",
"name": "windows_msvc_arm64_cross_release"
},
{
"type": "build",
"name": "windows_msvc_arm64_cross_release"
}
]
},
{
"name": "windows_msvc_ci",
"displayName": "Windows MSVC (CI)",
"steps": [
{
"type": "configure",
"name": "windows_msvc_ci"
},
{
"type": "build",
"name": "windows_msvc_ci"
},
{
"type": "test",
"name": "windows_msvc_ci"
}
]
},
{
"name": "windows_msvc_arm64_cross_ci",
"displayName": "Windows MSVC (ARM64 cross, CI)",
"steps": [
{
"type": "configure",
"name": "windows_msvc_arm64_cross_ci"
},
{
"type": "build",
"name": "windows_msvc_arm64_cross_ci"
}
]
}
]
}

View file

@ -0,0 +1,183 @@
{
"$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
"version": 8,
"include": [
"commonPresets.json"
],
"configurePresets": [
{
"name": "windows_mingw_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"generator": "Ninja",
"cacheVariables": {
"Launcher_BUILD_ARTIFACT": "Windows-MinGW-w64-Qt6"
}
},
{
"name": "windows_mingw_debug",
"inherits": [
"base_debug",
"windows_mingw_base"
],
"displayName": "Windows MinGW (Debug)"
},
{
"name": "windows_mingw_release",
"inherits": [
"base_release",
"windows_mingw_base"
],
"displayName": "Windows MinGW (Release)"
},
{
"name": "windows_mingw_ci",
"inherits": [
"base_ci",
"windows_mingw_base"
],
"displayName": "Windows MinGW (CI)",
"cacheVariables": {
"Launcher_BUILD_ARTIFACT": "Windows-MinGW-w64-Qt6"
}
}
],
"buildPresets": [
{
"name": "windows_mingw_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "windows_mingw_debug",
"inherits": [
"windows_mingw_base"
],
"displayName": "Windows MinGW (Debug)",
"configurePreset": "windows_mingw_debug"
},
{
"name": "windows_mingw_release",
"inherits": [
"windows_mingw_base"
],
"displayName": "Windows MinGW (Release)",
"configurePreset": "windows_mingw_release"
},
{
"name": "windows_mingw_ci",
"inherits": [
"windows_mingw_base"
],
"displayName": "Windows MinGW (CI)",
"configurePreset": "windows_mingw_ci"
}
],
"testPresets": [
{
"name": "windows_mingw_base",
"hidden": true,
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"filter": {
"exclude": {
"name": "^example64|example$"
}
}
},
{
"name": "windows_mingw_debug",
"inherits": [
"base_debug",
"windows_mingw_base"
],
"displayName": "Windows MinGW (Debug)",
"configurePreset": "windows_mingw_debug"
},
{
"name": "windows_mingw_release",
"inherits": [
"base_release",
"windows_mingw_base"
],
"displayName": "Windows MinGW (Release)",
"configurePreset": "windows_mingw_release"
},
{
"name": "windows_mingw_ci",
"inherits": [
"base_release",
"windows_mingw_base"
],
"displayName": "Windows MinGW (CI)",
"configurePreset": "windows_mingw_ci"
}
],
"workflowPresets": [
{
"name": "windows_mingw_debug",
"displayName": "Windows MinGW (Debug)",
"steps": [
{
"type": "configure",
"name": "windows_mingw_debug"
},
{
"type": "build",
"name": "windows_mingw_debug"
},
{
"type": "test",
"name": "windows_mingw_debug"
}
]
},
{
"name": "windows_mingw",
"displayName": "Windows MinGW (Release)",
"steps": [
{
"type": "configure",
"name": "windows_mingw_release"
},
{
"type": "build",
"name": "windows_mingw_release"
},
{
"type": "test",
"name": "windows_mingw_release"
}
]
},
{
"name": "windows_mingw_ci",
"displayName": "Windows MinGW (CI)",
"steps": [
{
"type": "configure",
"name": "windows_mingw_ci"
},
{
"type": "build",
"name": "windows_mingw_ci"
},
{
"type": "test",
"name": "windows_mingw_ci"
}
]
}
]
}

10
flake.lock generated
View file

@ -18,11 +18,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1744932701, "lastModified": 1748460289,
"narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=", "narHash": "sha256-7doLyJBzCllvqX4gszYtmZUToxKvMUrg45EUWaUYmBg=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef", "rev": "96ec055edbe5ee227f28cdbc3f1ddf1df5965102",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -32,7 +32,7 @@
"type": "github" "type": "github"
} }
}, },
"qt-qrcodegenerator": { "qrcodegenerator": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1737616857, "lastModified": 1737616857,
@ -52,7 +52,7 @@
"inputs": { "inputs": {
"libnbtplusplus": "libnbtplusplus", "libnbtplusplus": "libnbtplusplus",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"qt-qrcodegenerator": "qt-qrcodegenerator" "qrcodegenerator": "qrcodegenerator"
} }
} }
}, },

View file

@ -16,7 +16,7 @@
flake = false; flake = false;
}; };
qt-qrcodegenerator = { qrcodegenerator = {
url = "github:nayuki/QR-Code-generator"; url = "github:nayuki/QR-Code-generator";
flake = false; flake = false;
}; };
@ -27,7 +27,7 @@
self, self,
nixpkgs, nixpkgs,
libnbtplusplus, libnbtplusplus,
qt-qrcodegenerator, qrcodegenerator,
}: }:
let let
@ -175,7 +175,7 @@
prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix {
inherit inherit
libnbtplusplus libnbtplusplus
qt-qrcodegenerator qrcodegenerator
self self
; ;
}; };

View file

@ -128,6 +128,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <sys.h> #include <sys.h>
#include <QStringLiteral>
#include "SysInfo.h" #include "SysInfo.h"
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
@ -1887,17 +1888,6 @@ QString Application::getUserAgent()
return BuildConfig.USER_AGENT; return BuildConfig.USER_AGENT;
} }
QString Application::getUserAgentUncached()
{
QString uaOverride = m_settings->get("UserAgentOverride").toString();
if (!uaOverride.isEmpty()) {
uaOverride += " (Uncached)";
return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString());
}
return BuildConfig.USER_AGENT_UNCACHED;
}
bool Application::handleDataMigration(const QString& currentData, bool Application::handleDataMigration(const QString& currentData,
const QString& oldData, const QString& oldData,
const QString& name, const QString& name,

View file

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

View file

@ -310,6 +310,8 @@ set(MINECRAFT_SOURCES
minecraft/ParseUtils.h minecraft/ParseUtils.h
minecraft/ProfileUtils.cpp minecraft/ProfileUtils.cpp
minecraft/ProfileUtils.h minecraft/ProfileUtils.h
minecraft/ShortcutUtils.cpp
minecraft/ShortcutUtils.h
minecraft/Library.cpp minecraft/Library.cpp
minecraft/Library.h minecraft/Library.h
minecraft/MojangDownloadInfo.h minecraft/MojangDownloadInfo.h
@ -834,6 +836,10 @@ SET(LAUNCHER_SOURCES
icons/IconList.h icons/IconList.h
icons/IconList.cpp icons/IconList.cpp
# log utils
logs/AnonymizeLog.cpp
logs/AnonymizeLog.h
# GUI - windows # GUI - windows
ui/GuiUtil.h ui/GuiUtil.h
ui/GuiUtil.cpp ui/GuiUtil.cpp
@ -1048,6 +1054,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ProfileSetupDialog.h ui/dialogs/ProfileSetupDialog.h
ui/dialogs/CopyInstanceDialog.cpp ui/dialogs/CopyInstanceDialog.cpp
ui/dialogs/CopyInstanceDialog.h ui/dialogs/CopyInstanceDialog.h
ui/dialogs/CreateShortcutDialog.cpp
ui/dialogs/CreateShortcutDialog.h
ui/dialogs/CustomMessageBox.cpp ui/dialogs/CustomMessageBox.cpp
ui/dialogs/CustomMessageBox.h ui/dialogs/CustomMessageBox.h
ui/dialogs/ExportInstanceDialog.cpp ui/dialogs/ExportInstanceDialog.cpp
@ -1230,6 +1238,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/widgets/MinecraftSettingsWidget.ui ui/widgets/MinecraftSettingsWidget.ui
ui/widgets/JavaSettingsWidget.ui ui/widgets/JavaSettingsWidget.ui
ui/dialogs/CopyInstanceDialog.ui ui/dialogs/CopyInstanceDialog.ui
ui/dialogs/CreateShortcutDialog.ui
ui/dialogs/ProfileSetupDialog.ui ui/dialogs/ProfileSetupDialog.ui
ui/dialogs/ProgressDialog.ui ui/dialogs/ProgressDialog.ui
ui/dialogs/NewInstanceDialog.ui ui/dialogs/NewInstanceDialog.ui
@ -1306,7 +1315,7 @@ target_link_libraries(Launcher_logic
qdcss qdcss
BuildConfig BuildConfig
Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Widgets
qrcode qrcodegenerator
) )
if (UNIX AND NOT CYGWIN AND NOT APPLE) if (UNIX AND NOT CYGWIN AND NOT APPLE)

View file

@ -269,9 +269,9 @@ bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const
return m_ignoreFiles.contains(fileInfo.fileName()) || m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath())); return m_ignoreFiles.contains(fileInfo.fileName()) || m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath()));
} }
bool FileIgnoreProxy::filterFile(const QString& fileName) const bool FileIgnoreProxy::filterFile(const QFileInfo& file) const
{ {
return m_blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(m_root), fileName)); return m_blocked.covers(relPath(file.absoluteFilePath())) || ignoreFile(file);
} }
void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName) void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName)

View file

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

View file

@ -107,6 +107,10 @@ namespace fs = std::filesystem;
#if defined(__MINGW32__) #if defined(__MINGW32__)
// Avoid re-defining structs retroactively added to MinGW
// https://github.com/mingw-w64/mingw-w64/issues/90#issuecomment-2829284729
#if __MINGW64_VERSION_MAJOR < 13
struct _DUPLICATE_EXTENTS_DATA { struct _DUPLICATE_EXTENTS_DATA {
HANDLE FileHandle; HANDLE FileHandle;
LARGE_INTEGER SourceFileOffset; LARGE_INTEGER SourceFileOffset;
@ -116,6 +120,7 @@ struct _DUPLICATE_EXTENTS_DATA {
using DUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA; using DUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA;
using PDUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA*; using PDUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA*;
#endif
struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER { struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER {
WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32
@ -887,6 +892,11 @@ QString getDesktopDir()
return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
} }
QString getApplicationsDir()
{
return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
}
// Cross-platform Shortcut creation // Cross-platform Shortcut creation
bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon)
{ {
@ -898,16 +908,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri
return false; return false;
} }
#if defined(Q_OS_MACOS) #if defined(Q_OS_MACOS)
// Create the Application QDir application = destination + ".app/";
QDir applicationDirectory =
QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + "/" + BuildConfig.LAUNCHER_NAME + " Instances/";
if (!applicationDirectory.mkpath(".")) {
qWarning() << "Couldn't create application directory";
return false;
}
QDir application = applicationDirectory.path() + "/" + name + ".app/";
if (application.exists()) { if (application.exists()) {
qWarning() << "Application already exists!"; qWarning() << "Application already exists!";

View file

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

View file

@ -72,7 +72,6 @@ bool InstanceImportTask::abort()
bool wasAborted = false; bool wasAborted = false;
if (m_task) if (m_task)
wasAborted = m_task->abort(); wasAborted = m_task->abort();
Task::abort();
return wasAborted; return wasAborted;
} }
@ -212,6 +211,7 @@ void InstanceImportTask::processZipPack()
progressStep->status = status; progressStep->status = status;
stepProgress(*progressStep); stepProgress(*progressStep);
}); });
connect(zipTask.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); });
m_task.reset(zipTask); m_task.reset(zipTask);
zipTask->start(); zipTask->start();
} }
@ -263,6 +263,25 @@ void InstanceImportTask::extractFinished()
} }
} }
bool installIcon(QString root, QString instIcon)
{
auto importIconPath = IconUtils::findBestIconIn(root, instIcon);
if (importIconPath.isNull() || !QFile::exists(importIconPath))
importIconPath = IconUtils::findBestIconIn(root, "icon.png");
if (importIconPath.isNull() || !QFile::exists(importIconPath))
importIconPath = IconUtils::findBestIconIn(FS::PathCombine(root, "overrides"), "icon.png");
if (!importIconPath.isNull() && QFile::exists(importIconPath)) {
// import icon
auto iconList = APPLICATION->icons();
if (iconList->iconFileExists(instIcon)) {
iconList->deleteIcon(instIcon);
}
iconList->installIcon(importIconPath, instIcon);
return true;
}
return false;
}
void InstanceImportTask::processFlame() void InstanceImportTask::processFlame()
{ {
shared_qobject_ptr<FlameCreationTask> inst_creation_task = nullptr; shared_qobject_ptr<FlameCreationTask> inst_creation_task = nullptr;
@ -288,6 +307,14 @@ void InstanceImportTask::processFlame()
} }
inst_creation_task->setName(*this); inst_creation_task->setName(*this);
// if the icon was specified by user, use that. otherwise pull icon from the pack
if (m_instIcon == "default") {
auto iconKey = QString("Flame_%1_Icon").arg(name());
if (installIcon(m_stagingPath, iconKey)) {
m_instIcon = iconKey;
}
}
inst_creation_task->setIcon(m_instIcon); inst_creation_task->setIcon(m_instIcon);
inst_creation_task->setGroup(m_instGroup); inst_creation_task->setGroup(m_instGroup);
inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate());
@ -305,9 +332,11 @@ void InstanceImportTask::processFlame()
connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails);
connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted);
connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable);
connect(inst_creation_task.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); });
m_task.reset(inst_creation_task); m_task.reset(inst_creation_task);
setAbortable(true); setAbortable(true);
m_task->start(); m_task->start();
@ -340,17 +369,7 @@ void InstanceImportTask::processMultiMC()
} else { } else {
m_instIcon = instance.iconKey(); m_instIcon = instance.iconKey();
auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon); installIcon(instance.instanceRoot(), m_instIcon);
if (importIconPath.isNull() || !QFile::exists(importIconPath))
importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), "icon.png");
if (!importIconPath.isNull() && QFile::exists(importIconPath)) {
// import icon
auto iconList = APPLICATION->icons();
if (iconList->iconFileExists(m_instIcon)) {
iconList->deleteIcon(m_instIcon);
}
iconList->installIcon(importIconPath, m_instIcon);
}
} }
emitSucceeded(); emitSucceeded();
} }
@ -387,6 +406,14 @@ void InstanceImportTask::processModrinth()
} }
inst_creation_task->setName(*this); inst_creation_task->setName(*this);
// if the icon was specified by user, use that. otherwise pull icon from the pack
if (m_instIcon == "default") {
auto iconKey = QString("Modrinth_%1_Icon").arg(name());
if (installIcon(m_stagingPath, iconKey)) {
m_instIcon = iconKey;
}
}
inst_creation_task->setIcon(m_instIcon); inst_creation_task->setIcon(m_instIcon);
inst_creation_task->setGroup(m_instGroup); inst_creation_task->setGroup(m_instGroup);
inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate());
@ -404,9 +431,11 @@ void InstanceImportTask::processModrinth()
connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails);
connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted);
connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable);
connect(inst_creation_task.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); });
m_task.reset(inst_creation_task); m_task.reset(inst_creation_task);
setAbortable(true); setAbortable(true);
m_task->start(); m_task->start();

View file

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

View file

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

View file

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

View file

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

View file

@ -167,8 +167,8 @@ bool LogModel::isOverFlow()
return m_numLines >= m_maxLines && m_stopOnOverflow; return m_numLines >= m_maxLines && m_stopOnOverflow;
} }
MessageLevel::Enum LogModel::previousLevel()
MessageLevel::Enum LogModel::previousLevel() { {
if (!m_content.isEmpty()) { if (!m_content.isEmpty()) {
return m_content.last().level; return m_content.last().level;
} }

View file

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

View file

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

View file

@ -107,7 +107,7 @@ std::optional<LogParser::ParsedItem> LogParser::parseNext()
if (m_buffer.trimmed().isEmpty()) { if (m_buffer.trimmed().isEmpty()) {
auto text = QString(m_buffer); auto text = QString(m_buffer);
m_buffer.clear(); m_buffer.clear();
return LogParser::PlainText { text }; return LogParser::PlainText{ text };
} }
// check if we have a full xml log4j event // check if we have a full xml log4j event

View file

@ -248,6 +248,7 @@ void MinecraftInstance::loadSpecificSettings()
m_settings->registerSetting("ExportSummary", ""); m_settings->registerSetting("ExportSummary", "");
m_settings->registerSetting("ExportAuthor", ""); m_settings->registerSetting("ExportAuthor", "");
m_settings->registerSetting("ExportOptionalFiles", true); m_settings->registerSetting("ExportOptionalFiles", true);
m_settings->registerSetting("ExportRecommendedRAM");
auto dataPacksEnabled = m_settings->registerSetting("GlobalDataPacksEnabled", false); auto dataPacksEnabled = m_settings->registerSetting("GlobalDataPacksEnabled", false);
auto dataPacksPath = m_settings->registerSetting("GlobalDataPacksPath", ""); auto dataPacksPath = m_settings->registerSetting("GlobalDataPacksPath", "");
@ -1019,7 +1020,6 @@ QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSess
return filter; return filter;
} }
QStringList MinecraftInstance::getLogFileSearchPaths() QStringList MinecraftInstance::getLogFileSearchPaths()
{ {
return { FS::PathCombine(gameRoot(), "crash-reports"), FS::PathCombine(gameRoot(), "logs"), gameRoot() }; return { FS::PathCombine(gameRoot(), "crash-reports"), FS::PathCombine(gameRoot(), "logs"), gameRoot() };

View file

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

View file

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

View file

@ -84,7 +84,7 @@ class Mod : public Resource {
bool valid() const override; bool valid() const override;
[[nodiscard]] int compare(const Resource & other, SortType type) const override; [[nodiscard]] int compare(const Resource& other, SortType type) const override;
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
// Delete all the files of this mod // Delete all the files of this mod

View file

@ -48,7 +48,8 @@ TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, BaseInstance* in
m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size" }); m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE };
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true, true }; m_columnsHideable = { false, true, false, true, true, true };
m_columnsHiddenByDefault = { false, false, false, false, false, true }; m_columnsHiddenByDefault = { false, false, false, false, false, true };
} }

View file

@ -268,6 +268,26 @@ void SkinList::installSkins(const QStringList& iconFiles)
installSkin(file); installSkin(file);
} }
QString getUniqueFile(const QString& root, const QString& file)
{
auto result = FS::PathCombine(root, file);
if (!QFileInfo::exists(result)) {
return result;
}
QString baseName = QFileInfo(file).completeBaseName();
QString extension = QFileInfo(file).suffix();
int tries = 0;
while (QFileInfo::exists(result)) {
if (++tries > 256)
return {};
QString key = QString("%1%2.%3").arg(baseName).arg(tries).arg(extension);
result = FS::PathCombine(root, key);
}
return result;
}
QString SkinList::installSkin(const QString& file, const QString& name) QString SkinList::installSkin(const QString& file, const QString& name)
{ {
if (file.isEmpty()) if (file.isEmpty())
@ -282,7 +302,7 @@ QString SkinList::installSkin(const QString& file, const QString& name)
if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid()) if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid())
return tr("Skin images must be 64x64 or 64x32 pixel PNG files."); return tr("Skin images must be 64x64 or 64x32 pixel PNG files.");
QString target = FS::PathCombine(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name); QString target = getUniqueFile(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name);
return QFile::copy(file, target) ? "" : tr("Unable to copy file"); return QFile::copy(file, target) ? "" : tr("Unable to copy file");
} }
@ -371,7 +391,8 @@ bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role)
auto& skin = m_skinList[row]; auto& skin = m_skinList[row];
auto newName = value.toString(); auto newName = value.toString();
if (skin.name() != newName) { if (skin.name() != newName) {
skin.rename(newName); if (!skin.rename(newName))
return false;
save(); save();
} }
return true; return true;

View file

@ -122,7 +122,11 @@ QString SkinModel::name() const
bool SkinModel::rename(QString newName) bool SkinModel::rename(QString newName)
{ {
auto info = QFileInfo(m_path); auto info = QFileInfo(m_path);
m_path = FS::PathCombine(info.absolutePath(), newName + ".png"); auto new_path = FS::PathCombine(info.absolutePath(), newName + ".png");
if (QFileInfo::exists(new_path)) {
return false;
}
m_path = new_path;
return FS::move(info.absoluteFilePath(), m_path); return FS::move(info.absoluteFilePath(), m_path);
} }

View file

@ -54,6 +54,7 @@
#include "settings/INISettingsObject.h" #include "settings/INISettingsObject.h"
#include "sys.h"
#include "tasks/ConcurrentTask.h" #include "tasks/ConcurrentTask.h"
#include "ui/dialogs/BlockedModsDialog.h" #include "ui/dialogs/BlockedModsDialog.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
@ -418,6 +419,24 @@ bool FlameCreationTask::createInstance()
} }
} }
int recommendedRAM = m_pack.minecraft.recommendedRAM;
// only set memory if this is a fresh instance
if (m_instance == nullptr && recommendedRAM > 0) {
const uint64_t sysMiB = Sys::getSystemRam() / Sys::mebibyte;
const uint64_t max = sysMiB * 0.9;
if (recommendedRAM > max) {
logWarning(tr("The recommended memory of the modpack exceeds 90% of your system RAM—reducing it from %1 MiB to %2 MiB!")
.arg(recommendedRAM)
.arg(max));
recommendedRAM = max;
}
instance.settings()->set("OverrideMemory", true);
instance.settings()->set("MaxMemAlloc", recommendedRAM);
}
QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods"); QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods");
QFileInfo jarmodsInfo(jarmodsPath); QFileInfo jarmodsInfo(jarmodsPath);
if (jarmodsInfo.isDir()) { if (jarmodsInfo.isDir()) {

View file

@ -41,22 +41,8 @@
const QString FlamePackExportTask::TEMPLATE = "<li><a href=\"{url}\">{name}{authors}</a></li>\n"; const QString FlamePackExportTask::TEMPLATE = "<li><a href=\"{url}\">{name}{authors}</a></li>\n";
const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" }); const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" });
FlamePackExportTask::FlamePackExportTask(const QString& name, FlamePackExportTask::FlamePackExportTask(FlamePackExportOptions&& options)
const QString& version, : m_options(std::move(options)), m_gameRoot(m_options.instance->gameRoot())
const QString& author,
bool optionalFiles,
InstancePtr instance,
const QString& output,
MMCZip::FilterFunction filter)
: name(name)
, version(version)
, author(author)
, optionalFiles(optionalFiles)
, instance(instance)
, mcInstance(dynamic_cast<MinecraftInstance*>(instance.get()))
, gameRoot(instance->gameRoot())
, output(output)
, filter(filter)
{} {}
void FlamePackExportTask::executeTask() void FlamePackExportTask::executeTask()
@ -70,7 +56,6 @@ bool FlamePackExportTask::abort()
{ {
if (task) { if (task) {
task->abort(); task->abort();
emitAborted();
return true; return true;
} }
return false; return false;
@ -82,7 +67,7 @@ void FlamePackExportTask::collectFiles()
QCoreApplication::processEvents(); QCoreApplication::processEvents();
files.clear(); files.clear();
if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) { if (!MMCZip::collectFileListRecursively(m_options.instance->gameRoot(), nullptr, &files, m_options.filter)) {
emitFailed(tr("Could not search for files")); emitFailed(tr("Could not search for files"));
return; return;
} }
@ -90,11 +75,8 @@ void FlamePackExportTask::collectFiles()
pendingHashes.clear(); pendingHashes.clear();
resolvedFiles.clear(); resolvedFiles.clear();
if (mcInstance != nullptr) { m_options.instance->loaderModList()->update();
mcInstance->loaderModList()->update(); connect(m_options.instance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes);
connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes);
} else
collectHashes();
} }
void FlamePackExportTask::collectHashes() void FlamePackExportTask::collectHashes()
@ -102,11 +84,11 @@ void FlamePackExportTask::collectHashes()
setAbortable(true); setAbortable(true);
setStatus(tr("Finding file hashes...")); setStatus(tr("Finding file hashes..."));
setProgress(1, 5); setProgress(1, 5);
auto allMods = mcInstance->loaderModList()->allMods(); auto allMods = m_options.instance->loaderModList()->allMods();
ConcurrentTask::Ptr hashingTask(new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); ConcurrentTask::Ptr hashingTask(new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()));
task.reset(hashingTask); task.reset(hashingTask);
for (const QFileInfo& file : files) { for (const QFileInfo& file : files) {
const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath()); const QString relative = m_gameRoot.relativeFilePath(file.absoluteFilePath());
// require sensible file types // require sensible file types
if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) { if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) {
return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled"); return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled");
@ -171,6 +153,7 @@ void FlamePackExportTask::collectHashes()
progressStep->status = status; progressStep->status = status;
stepProgress(*progressStep); stepProgress(*progressStep);
}); });
connect(hashingTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted);
hashingTask->start(); hashingTask->start();
} }
@ -246,6 +229,7 @@ void FlamePackExportTask::makeApiRequest()
getProjectsInfo(); getProjectsInfo();
}); });
connect(task.get(), &Task::failed, this, &FlamePackExportTask::getProjectsInfo); connect(task.get(), &Task::failed, this, &FlamePackExportTask::getProjectsInfo);
connect(task.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted);
task->start(); task->start();
} }
@ -324,6 +308,7 @@ void FlamePackExportTask::getProjectsInfo()
buildZip(); buildZip();
}); });
connect(projTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); connect(projTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
connect(projTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted);
task.reset(projTask); task.reset(projTask);
task->start(); task->start();
} }
@ -333,13 +318,13 @@ void FlamePackExportTask::buildZip()
setStatus(tr("Adding files...")); setStatus(tr("Adding files..."));
setProgress(4, 5); setProgress(4, 5);
auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true, false); auto zipTask = makeShared<MMCZip::ExportToZipTask>(m_options.output, m_gameRoot, files, "overrides/", true, false);
zipTask->addExtraFile("manifest.json", generateIndex()); zipTask->addExtraFile("manifest.json", generateIndex());
zipTask->addExtraFile("modlist.html", generateHTML()); zipTask->addExtraFile("modlist.html", generateHTML());
QStringList exclude; QStringList exclude;
std::transform(resolvedFiles.keyBegin(), resolvedFiles.keyEnd(), std::back_insert_iterator(exclude), std::transform(resolvedFiles.keyBegin(), resolvedFiles.keyEnd(), std::back_insert_iterator(exclude),
[this](QString file) { return gameRoot.relativeFilePath(file); }); [this](QString file) { return m_gameRoot.relativeFilePath(file); });
zipTask->setExcludeFiles(exclude); zipTask->setExcludeFiles(exclude);
auto progressStep = std::make_shared<TaskStepProgress>(); auto progressStep = std::make_shared<TaskStepProgress>();
@ -374,52 +359,56 @@ QByteArray FlamePackExportTask::generateIndex()
QJsonObject obj; QJsonObject obj;
obj["manifestType"] = "minecraftModpack"; obj["manifestType"] = "minecraftModpack";
obj["manifestVersion"] = 1; obj["manifestVersion"] = 1;
obj["name"] = name; obj["name"] = m_options.name;
obj["version"] = version; obj["version"] = m_options.version;
obj["author"] = author; obj["author"] = m_options.author;
obj["overrides"] = "overrides"; obj["overrides"] = "overrides";
if (mcInstance) {
QJsonObject version;
auto profile = mcInstance->getPackProfile();
// collect all supported components
const ComponentPtr minecraft = profile->getComponent("net.minecraft");
const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
const ComponentPtr forge = profile->getComponent("net.minecraftforge");
const ComponentPtr neoforge = profile->getComponent("net.neoforged");
// convert all available components to mrpack dependencies QJsonObject version;
if (minecraft != nullptr)
version["version"] = minecraft->m_version; auto profile = m_options.instance->getPackProfile();
QString id; // collect all supported components
if (quilt != nullptr) const ComponentPtr minecraft = profile->getComponent("net.minecraft");
id = "quilt-" + quilt->m_version; const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
else if (fabric != nullptr) const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
id = "fabric-" + fabric->m_version; const ComponentPtr forge = profile->getComponent("net.minecraftforge");
else if (forge != nullptr) const ComponentPtr neoforge = profile->getComponent("net.neoforged");
id = "forge-" + forge->m_version;
else if (neoforge != nullptr) { // convert all available components to mrpack dependencies
id = "neoforge-"; if (minecraft != nullptr)
if (minecraft->m_version == "1.20.1") version["version"] = minecraft->m_version;
id += "1.20.1-"; QString id;
id += neoforge->m_version; if (quilt != nullptr)
} id = "quilt-" + quilt->m_version;
version["modLoaders"] = QJsonArray(); else if (fabric != nullptr)
if (!id.isEmpty()) { id = "fabric-" + fabric->m_version;
QJsonObject loader; else if (forge != nullptr)
loader["id"] = id; id = "forge-" + forge->m_version;
loader["primary"] = true; else if (neoforge != nullptr) {
version["modLoaders"] = QJsonArray({ loader }); id = "neoforge-";
} if (minecraft->m_version == "1.20.1")
obj["minecraft"] = version; id += "1.20.1-";
id += neoforge->m_version;
} }
version["modLoaders"] = QJsonArray();
if (!id.isEmpty()) {
QJsonObject loader;
loader["id"] = id;
loader["primary"] = true;
version["modLoaders"] = QJsonArray({ loader });
}
if (m_options.recommendedRAM > 0)
version["recommendedRam"] = m_options.recommendedRAM;
obj["minecraft"] = version;
QJsonArray files; QJsonArray files;
for (auto mod : resolvedFiles) { for (auto mod : resolvedFiles) {
QJsonObject file; QJsonObject file;
file["projectID"] = mod.addonId; file["projectID"] = mod.addonId;
file["fileID"] = mod.version; file["fileID"] = mod.version;
file["required"] = mod.enabled || !optionalFiles; file["required"] = mod.enabled || !m_options.optionalFiles;
files << file; files << file;
} }
obj["files"] = files; obj["files"] = files;

View file

@ -19,22 +19,26 @@
#pragma once #pragma once
#include "BaseInstance.h"
#include "MMCZip.h" #include "MMCZip.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameAPI.h"
#include "tasks/Task.h" #include "tasks/Task.h"
struct FlamePackExportOptions {
QString name;
QString version;
QString author;
bool optionalFiles;
MinecraftInstancePtr instance;
QString output;
MMCZip::FilterFileFunction filter;
int recommendedRAM;
};
class FlamePackExportTask : public Task { class FlamePackExportTask : public Task {
Q_OBJECT Q_OBJECT
public: public:
FlamePackExportTask(const QString& name, FlamePackExportTask(FlamePackExportOptions&& options);
const QString& version,
const QString& author,
bool optionalFiles,
InstancePtr instance,
const QString& output,
MMCZip::FilterFunction filter);
protected: protected:
void executeTask() override; void executeTask() override;
@ -45,13 +49,6 @@ class FlamePackExportTask : public Task {
static const QStringList FILE_EXTENSIONS; static const QStringList FILE_EXTENSIONS;
// inputs // inputs
const QString name, version, author;
const bool optionalFiles;
const InstancePtr instance;
MinecraftInstance* mcInstance;
const QDir gameRoot;
const QString output;
const MMCZip::FilterFunction filter;
struct ResolvedFile { struct ResolvedFile {
int addonId; int addonId;
@ -70,6 +67,9 @@ class FlamePackExportTask : public Task {
bool isMod; bool isMod;
}; };
FlamePackExportOptions m_options;
QDir m_gameRoot;
FlameAPI api; FlameAPI api;
QFileInfoList files; QFileInfoList files;

View file

@ -27,6 +27,7 @@ static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft)
loadModloaderV1(loader, obj); loadModloaderV1(loader, obj);
m.modLoaders.append(loader); m.modLoaders.append(loader);
} }
m.recommendedRAM = Json::ensureInteger(minecraft, "recommendedRam", 0);
} }
static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest) static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest)

View file

@ -67,6 +67,7 @@ struct Minecraft {
QString version; QString version;
QString libraries; QString libraries;
QList<Flame::Modloader> modLoaders; QList<Flame::Modloader> modLoaders;
int recommendedRAM;
}; };
struct Manifest { struct Manifest {

View file

@ -40,7 +40,7 @@ ModrinthPackExportTask::ModrinthPackExportTask(const QString& name,
bool optionalFiles, bool optionalFiles,
InstancePtr instance, InstancePtr instance,
const QString& output, const QString& output,
MMCZip::FilterFunction filter) MMCZip::FilterFileFunction filter)
: name(name) : name(name)
, version(version) , version(version)
, summary(summary) , summary(summary)
@ -63,7 +63,6 @@ bool ModrinthPackExportTask::abort()
{ {
if (task) { if (task) {
task->abort(); task->abort();
emitAborted();
return true; return true;
} }
return false; return false;
@ -158,6 +157,7 @@ void ModrinthPackExportTask::makeApiRequest()
task = api.currentVersions(pendingHashes.values(), "sha512", response); task = api.currentVersions(pendingHashes.values(), "sha512", response);
connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response); }); connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response); });
connect(task.get(), &Task::failed, this, &ModrinthPackExportTask::emitFailed); connect(task.get(), &Task::failed, this, &ModrinthPackExportTask::emitFailed);
connect(task.get(), &Task::aborted, this, &ModrinthPackExportTask::emitAborted);
task->start(); task->start();
} }
} }

View file

@ -35,7 +35,7 @@ class ModrinthPackExportTask : public Task {
bool optionalFiles, bool optionalFiles,
InstancePtr instance, InstancePtr instance,
const QString& output, const QString& output,
MMCZip::FilterFunction filter); MMCZip::FilterFileFunction filter);
protected: protected:
void executeTask() override; void executeTask() override;
@ -58,7 +58,7 @@ class ModrinthPackExportTask : public Task {
MinecraftInstance* mcInstance; MinecraftInstance* mcInstance;
const QDir gameRoot; const QDir gameRoot;
const QString output; const QString output;
const MMCZip::FilterFunction filter; const MMCZip::FilterFileFunction filter;
ModrinthAPI api; ModrinthAPI api;
QFileInfoList files; QFileInfoList files;

View file

@ -58,6 +58,7 @@ class ByteArraySink : public Sink {
qWarning() << "ByteArraySink did not initialize the buffer because it's not addressable"; qWarning() << "ByteArraySink did not initialize the buffer because it's not addressable";
if (initAllValidators(request)) if (initAllValidators(request))
return Task::State::Running; return Task::State::Running;
m_fail_reason = "Failed to initialize validators";
return Task::State::Failed; return Task::State::Failed;
}; };
@ -69,12 +70,14 @@ class ByteArraySink : public Sink {
qWarning() << "ByteArraySink did not write the buffer because it's not addressable"; qWarning() << "ByteArraySink did not write the buffer because it's not addressable";
if (writeAllValidators(data)) if (writeAllValidators(data))
return Task::State::Running; return Task::State::Running;
m_fail_reason = "Failed to write validators";
return Task::State::Failed; return Task::State::Failed;
} }
auto abort() -> Task::State override auto abort() -> Task::State override
{ {
failAllValidators(); failAllValidators();
m_fail_reason = "Aborted";
return Task::State::Failed; return Task::State::Failed;
} }
@ -82,12 +85,13 @@ class ByteArraySink : public Sink {
{ {
if (finalizeAllValidators(reply)) if (finalizeAllValidators(reply))
return Task::State::Succeeded; return Task::State::Succeeded;
m_fail_reason = "Failed to finalize validators";
return Task::State::Failed; return Task::State::Failed;
} }
auto hasLocalData() -> bool override { return false; } auto hasLocalData() -> bool override { return false; }
private: protected:
std::shared_ptr<QByteArray> m_output; std::shared_ptr<QByteArray> m_output;
}; };
} // namespace Net } // namespace Net

View file

@ -51,6 +51,7 @@ Task::State FileSink::init(QNetworkRequest& request)
// create a new save file and open it for writing // create a new save file and open it for writing
if (!FS::ensureFilePathExists(m_filename)) { if (!FS::ensureFilePathExists(m_filename)) {
qCCritical(taskNetLogC) << "Could not create folder for " + m_filename; qCCritical(taskNetLogC) << "Could not create folder for " + m_filename;
m_fail_reason = "Could not create folder";
return Task::State::Failed; return Task::State::Failed;
} }
@ -58,11 +59,13 @@ Task::State FileSink::init(QNetworkRequest& request)
m_output_file.reset(new PSaveFile(m_filename)); m_output_file.reset(new PSaveFile(m_filename));
if (!m_output_file->open(QIODevice::WriteOnly)) { if (!m_output_file->open(QIODevice::WriteOnly)) {
qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing"; qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing";
m_fail_reason = "Could not open file";
return Task::State::Failed; return Task::State::Failed;
} }
if (initAllValidators(request)) if (initAllValidators(request))
return Task::State::Running; return Task::State::Running;
m_fail_reason = "Failed to initialize validators";
return Task::State::Failed; return Task::State::Failed;
} }
@ -73,6 +76,7 @@ Task::State FileSink::write(QByteArray& data)
m_output_file->cancelWriting(); m_output_file->cancelWriting();
m_output_file.reset(); m_output_file.reset();
m_wroteAnyData = false; m_wroteAnyData = false;
m_fail_reason = "Failed to write validators";
return Task::State::Failed; return Task::State::Failed;
} }
@ -105,13 +109,16 @@ Task::State FileSink::finalize(QNetworkReply& reply)
if (gotFile || m_wroteAnyData) { if (gotFile || m_wroteAnyData) {
// ask validators for data consistency // ask validators for data consistency
// we only do this for actual downloads, not 'your data is still the same' cache hits // we only do this for actual downloads, not 'your data is still the same' cache hits
if (!finalizeAllValidators(reply)) if (!finalizeAllValidators(reply)) {
m_fail_reason = "Failed to finalize validators";
return Task::State::Failed; return Task::State::Failed;
}
// nothing went wrong... // nothing went wrong...
if (!m_output_file->commit()) { if (!m_output_file->commit()) {
qCCritical(taskNetLogC) << "Failed to commit changes to " << m_filename; qCCritical(taskNetLogC) << "Failed to commit changes to " << m_filename;
m_output_file->cancelWriting(); m_output_file->cancelWriting();
m_fail_reason = "Failed to commit changes";
return Task::State::Failed; return Task::State::Failed;
} }
} }

View file

@ -166,7 +166,7 @@ auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool
return true; return true;
} }
//returns true on success, false otherwise // returns true on success, false otherwise
auto HttpMetaCache::evictAll() -> bool auto HttpMetaCache::evictAll() -> bool
{ {
bool ret = true; bool ret = true;
@ -178,7 +178,7 @@ auto HttpMetaCache::evictAll() -> bool
qCWarning(taskHttpMetaCacheLogC) << "Unexpected missing cache entry" << entry->m_basePath; qCWarning(taskHttpMetaCacheLogC) << "Unexpected missing cache entry" << entry->m_basePath;
} }
map.entry_list.clear(); map.entry_list.clear();
//AND all return codes together so the result is true iff all runs of deletePath() are true // AND all return codes together so the result is true iff all runs of deletePath() are true
ret &= FS::deletePath(map.base_path); ret &= FS::deletePath(map.base_path);
} }
return ret; return ret;

View file

@ -84,7 +84,8 @@ void NetRequest::executeTask()
break; break;
case State::Inactive: case State::Inactive:
case State::Failed: case State::Failed:
emit failed("Failed to initialize sink"); m_failReason = m_sink->failReason();
emit failed(m_sink->failReason());
emit finished(); emit finished();
return; return;
case State::AbortedByUser: case State::AbortedByUser:
@ -259,6 +260,7 @@ void NetRequest::downloadFinished()
} else if (m_state == State::Failed) { } else if (m_state == State::Failed) {
qCDebug(logCat) << getUid().toString() << "Request failed in previous step:" << m_url.toString(); qCDebug(logCat) << getUid().toString() << "Request failed in previous step:" << m_url.toString();
m_sink->abort(); m_sink->abort();
m_failReason = m_reply->errorString();
emit failed(m_reply->errorString()); emit failed(m_reply->errorString());
emit finished(); emit finished();
return; return;
@ -278,7 +280,8 @@ void NetRequest::downloadFinished()
if (m_state != State::Succeeded) { if (m_state != State::Succeeded) {
qCDebug(logCat) << getUid().toString() << "Request failed to write:" << m_url.toString(); qCDebug(logCat) << getUid().toString() << "Request failed to write:" << m_url.toString();
m_sink->abort(); m_sink->abort();
emit failed("failed to write in sink"); m_failReason = m_sink->failReason();
emit failed(m_sink->failReason());
emit finished(); emit finished();
return; return;
} }
@ -289,7 +292,8 @@ void NetRequest::downloadFinished()
if (m_state != State::Succeeded) { if (m_state != State::Succeeded) {
qCDebug(logCat) << getUid().toString() << "Request failed to finalize:" << m_url.toString(); qCDebug(logCat) << getUid().toString() << "Request failed to finalize:" << m_url.toString();
m_sink->abort(); m_sink->abort();
emit failed("failed to finalize the request"); m_failReason = m_sink->failReason();
emit failed(m_sink->failReason());
emit finished(); emit finished();
return; return;
} }
@ -305,7 +309,7 @@ void NetRequest::downloadReadyRead()
auto data = m_reply->readAll(); auto data = m_reply->readAll();
m_state = m_sink->write(data); m_state = m_sink->write(data);
if (m_state == State::Failed) { if (m_state == State::Failed) {
qCCritical(logCat) << getUid().toString() << "Failed to process response chunk"; qCCritical(logCat) << getUid().toString() << "Failed to process response chunk:" << m_sink->failReason();
} }
// qDebug() << "Request" << m_url.toString() << "gained" << data.size() << "bytes"; // qDebug() << "Request" << m_url.toString() << "gained" << data.size() << "bytes";
} else { } else {

View file

@ -36,74 +36,45 @@
*/ */
#include "PasteUpload.h" #include "PasteUpload.h"
#include "Application.h" #include <qobject.h>
#include "BuildConfig.h"
#include <QDebug>
#include <QFile>
#include <QHttpPart> #include <QHttpPart>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QRegularExpression>
#include <QUrlQuery> #include <QUrlQuery>
#include "logs/AnonymizeLog.h"
#include "net/Logging.h" const std::array<PasteUpload::PasteTypeInfo, 4> PasteUpload::PasteTypes = { { { "0x0.st", "https://0x0.st", "" },
{ "hastebin", "https://hst.sh", "/documents" },
{ "paste.gg", "https://paste.gg", "/api/v1/pastes" },
{ "mclo.gs", "https://api.mclo.gs", "/1/log" } } };
std::array<PasteUpload::PasteTypeInfo, 4> PasteUpload::PasteTypes = { { { "0x0.st", "https://0x0.st", "" }, QNetworkReply* PasteUpload::getReply(QNetworkRequest& request)
{ "hastebin", "https://hst.sh", "/documents" },
{ "paste.gg", "https://paste.gg", "/api/v1/pastes" },
{ "mclo.gs", "https://api.mclo.gs", "/1/log" } } };
PasteUpload::PasteUpload(QWidget* window, QString text, QString baseUrl, PasteType pasteType)
: m_window(window), m_baseUrl(baseUrl), m_pasteType(pasteType), m_text(text.toUtf8())
{ {
if (m_baseUrl == "") switch (m_paste_type) {
m_baseUrl = PasteTypes.at(pasteType).defaultBase; case PasteUpload::NullPointer: {
QHttpMultiPart* multiPart = new QHttpMultiPart{ QHttpMultiPart::FormDataType, this };
// HACK: Paste's docs say the standard API path is at /api/<version> but the official instance paste.gg doesn't follow that??
if (pasteType == PasteGG && m_baseUrl == PasteTypes.at(pasteType).defaultBase)
m_uploadUrl = "https://api.paste.gg/v1/pastes";
else
m_uploadUrl = m_baseUrl + PasteTypes.at(pasteType).endpointPath;
}
PasteUpload::~PasteUpload() {}
void PasteUpload::executeTask()
{
QNetworkRequest request{ QUrl(m_uploadUrl) };
QNetworkReply* rep{};
request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8());
switch (m_pasteType) {
case NullPointer: {
QHttpMultiPart* multiPart = new QHttpMultiPart{ QHttpMultiPart::FormDataType };
QHttpPart filePart; QHttpPart filePart;
filePart.setBody(m_text); filePart.setBody(m_log.toUtf8());
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain");
filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"file\"; filename=\"log.txt\""); filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"file\"; filename=\"log.txt\"");
multiPart->append(filePart); multiPart->append(filePart);
rep = APPLICATION->network()->post(request, multiPart); return m_network->post(request, multiPart);
multiPart->setParent(rep);
break;
} }
case Hastebin: { case PasteUpload::Hastebin: {
request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); return m_network->post(request, m_log.toUtf8());
rep = APPLICATION->network()->post(request, m_text);
break;
} }
case Mclogs: { case PasteUpload::Mclogs: {
QUrlQuery postData; QUrlQuery postData;
postData.addQueryItem("content", m_text); postData.addQueryItem("content", m_log);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
rep = APPLICATION->network()->post(request, postData.toString().toUtf8()); return m_network->post(request, postData.toString().toUtf8());
break;
} }
case PasteGG: { case PasteUpload::PasteGG: {
QJsonObject obj; QJsonObject obj;
QJsonDocument doc; QJsonDocument doc;
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
@ -114,7 +85,7 @@ void PasteUpload::executeTask()
QJsonObject logFileInfo; QJsonObject logFileInfo;
QJsonObject logFileContentInfo; QJsonObject logFileContentInfo;
logFileContentInfo.insert("format", "text"); logFileContentInfo.insert("format", "text");
logFileContentInfo.insert("value", QString::fromUtf8(m_text)); logFileContentInfo.insert("value", m_log);
logFileInfo.insert("name", "log.txt"); logFileInfo.insert("name", "log.txt");
logFileInfo.insert("content", logFileContentInfo); logFileInfo.insert("content", logFileContentInfo);
files.append(logFileInfo); files.append(logFileInfo);
@ -122,108 +93,127 @@ void PasteUpload::executeTask()
obj.insert("files", files); obj.insert("files", files);
doc.setObject(obj); doc.setObject(obj);
rep = APPLICATION->network()->post(request, doc.toJson()); return m_network->post(request, doc.toJson());
break;
} }
} }
connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); return nullptr;
connect(rep, &QNetworkReply::finished, this, &PasteUpload::downloadFinished); };
connect(rep, &QNetworkReply::errorOccurred, this, &PasteUpload::downloadError); auto PasteUpload::Sink::finalize(QNetworkReply& reply) -> Task::State
m_reply = std::shared_ptr<QNetworkReply>(rep);
setStatus(tr("Uploading to %1").arg(m_uploadUrl));
}
void PasteUpload::downloadError(QNetworkReply::NetworkError error)
{ {
// error happened during download. if (!finalizeAllValidators(reply)) {
qCCritical(taskUploadLogC) << getUid().toString() << "Network error: " << error; m_fail_reason = "Failed to finalize validators";
emitFailed(m_reply->errorString()); return Task::State::Failed;
} }
int statusCode = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
void PasteUpload::downloadFinished() if (reply.error() != QNetworkReply::NetworkError::NoError) {
{ m_fail_reason = QObject::tr("Network error: %1").arg(reply.errorString());
QByteArray data = m_reply->readAll(); return Task::State::Failed;
int statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (m_reply->error() != QNetworkReply::NetworkError::NoError) {
emitFailed(tr("Network error: %1").arg(m_reply->errorString()));
m_reply.reset();
return;
} else if (statusCode != 200 && statusCode != 201) { } else if (statusCode != 200 && statusCode != 201) {
QString reasonPhrase = m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); QString reasonPhrase = reply.attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
emitFailed(tr("Error: %1 returned unexpected status code %2 %3").arg(m_uploadUrl).arg(statusCode).arg(reasonPhrase)); m_fail_reason =
qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned unexpected status code " << statusCode QObject::tr("Error: %1 returned unexpected status code %2 %3").arg(m_d->url().toString()).arg(statusCode).arg(reasonPhrase);
<< " with body: " << data; return Task::State::Failed;
m_reply.reset();
return;
} }
switch (m_pasteType) { switch (m_d->m_paste_type) {
case NullPointer: case PasteUpload::NullPointer:
m_pasteLink = QString::fromUtf8(data).trimmed(); m_d->m_pasteLink = QString::fromUtf8(*m_output).trimmed();
break; break;
case Hastebin: { case PasteUpload::Hastebin: {
QJsonDocument jsonDoc{ QJsonDocument::fromJson(data) }; QJsonParseError jsonError;
QJsonObject jsonObj{ jsonDoc.object() }; auto doc = QJsonDocument::fromJson(*m_output, &jsonError);
if (jsonObj.contains("key") && jsonObj["key"].isString()) { if (jsonError.error != QJsonParseError::NoError) {
QString key = jsonDoc.object()["key"].toString(); qDebug() << "hastebin server did not reply with JSON" << jsonError.errorString();
m_pasteLink = m_baseUrl + "/" + key; m_fail_reason =
QObject::tr("Failed to parse response from hastebin server: expected JSON but got an invalid response. Error: %1")
.arg(jsonError.errorString());
return Task::State::Failed;
}
auto obj = doc.object();
if (obj.contains("key") && obj["key"].isString()) {
QString key = doc.object()["key"].toString();
m_d->m_pasteLink = m_d->m_baseUrl + "/" + key;
} else { } else {
emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); qDebug() << "Log upload failed:" << doc.toJson();
qCCritical(taskUploadLogC) << getUid().toString() << getUid().toString() << m_uploadUrl m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString());
<< " returned malformed response body: " << data; return Task::State::Failed;
return;
} }
break; break;
} }
case Mclogs: { case PasteUpload::Mclogs: {
QJsonDocument jsonDoc{ QJsonDocument::fromJson(data) }; QJsonParseError jsonError;
QJsonObject jsonObj{ jsonDoc.object() }; auto doc = QJsonDocument::fromJson(*m_output, &jsonError);
if (jsonObj.contains("success") && jsonObj["success"].isBool()) { if (jsonError.error != QJsonParseError::NoError) {
bool success = jsonObj["success"].toBool(); qDebug() << "mclogs server did not reply with JSON" << jsonError.errorString();
m_fail_reason =
QObject::tr("Failed to parse response from mclogs server: expected JSON but got an invalid response. Error: %1")
.arg(jsonError.errorString());
return Task::State::Failed;
}
auto obj = doc.object();
if (obj.contains("success") && obj["success"].isBool()) {
bool success = obj["success"].toBool();
if (success) { if (success) {
m_pasteLink = jsonObj["url"].toString(); m_d->m_pasteLink = obj["url"].toString();
} else { } else {
QString error = jsonObj["error"].toString(); QString error = obj["error"].toString();
emitFailed(tr("Error: %1 returned an error: %2").arg(m_uploadUrl, error)); m_fail_reason = QObject::tr("Error: %1 returned an error: %2").arg(m_d->url().toString(), error);
qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned error: " << error; return Task::State::Failed;
qCCritical(taskUploadLogC) << getUid().toString() << "Response body: " << data;
return;
} }
} else { } else {
emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); qDebug() << "Log upload failed:" << doc.toJson();
qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned malformed response body: " << data; m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString());
return; return Task::State::Failed;
} }
break; break;
} }
case PasteGG: case PasteUpload::PasteGG:
QJsonDocument jsonDoc{ QJsonDocument::fromJson(data) }; QJsonParseError jsonError;
QJsonObject jsonObj{ jsonDoc.object() }; auto doc = QJsonDocument::fromJson(*m_output, &jsonError);
if (jsonObj.contains("status") && jsonObj["status"].isString()) { if (jsonError.error != QJsonParseError::NoError) {
QString status = jsonObj["status"].toString(); qDebug() << "pastegg server did not reply with JSON" << jsonError.errorString();
m_fail_reason =
QObject::tr("Failed to parse response from pasteGG server: expected JSON but got an invalid response. Error: %1")
.arg(jsonError.errorString());
return Task::State::Failed;
}
auto obj = doc.object();
if (obj.contains("status") && obj["status"].isString()) {
QString status = obj["status"].toString();
if (status == "success") { if (status == "success") {
m_pasteLink = m_baseUrl + "/p/anonymous/" + jsonObj["result"].toObject()["id"].toString(); m_d->m_pasteLink = m_d->m_baseUrl + "/p/anonymous/" + obj["result"].toObject()["id"].toString();
} else { } else {
QString error = jsonObj["error"].toString(); QString error = obj["error"].toString();
QString message = QString message = (obj.contains("message") && obj["message"].isString()) ? obj["message"].toString() : "none";
(jsonObj.contains("message") && jsonObj["message"].isString()) ? jsonObj["message"].toString() : "none"; m_fail_reason =
emitFailed(tr("Error: %1 returned an error code: %2\nError message: %3").arg(m_uploadUrl, error, message)); QObject::tr("Error: %1 returned an error code: %2\nError message: %3").arg(m_d->url().toString(), error, message);
qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned error: " << error; return Task::State::Failed;
qCCritical(taskUploadLogC) << getUid().toString() << "Error message: " << message;
qCCritical(taskUploadLogC) << getUid().toString() << "Response body: " << data;
return;
} }
} else { } else {
emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); qDebug() << "Log upload failed:" << doc.toJson();
qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned malformed response body: " << data; m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString());
return; return Task::State::Failed;
} }
break; break;
} }
emitSucceeded(); return Task::State::Succeeded;
}
PasteUpload::PasteUpload(const QString& log, QString url, PasteType pasteType) : m_log(log), m_baseUrl(url), m_paste_type(pasteType)
{
anonymizeLog(m_log);
auto base = PasteUpload::PasteTypes.at(pasteType);
if (m_baseUrl.isEmpty())
m_baseUrl = base.defaultBase;
// HACK: Paste's docs say the standard API path is at /api/<version> but the official instance paste.gg doesn't follow that??
if (pasteType == PasteUpload::PasteGG && m_baseUrl == base.defaultBase)
m_url = "https://api.paste.gg/v1/pastes";
else
m_url = m_baseUrl + base.endpointPath;
m_sink.reset(new Sink(this));
} }

View file

@ -35,15 +35,18 @@
#pragma once #pragma once
#include <QBuffer> #include "net/ByteArraySink.h"
#include <QNetworkReply> #include "net/NetRequest.h"
#include <QString>
#include <array>
#include <memory>
#include "tasks/Task.h" #include "tasks/Task.h"
class PasteUpload : public Task { #include <QNetworkReply>
Q_OBJECT #include <QRegularExpression>
#include <QString>
#include <array>
#include <memory>
class PasteUpload : public Net::NetRequest {
public: public:
enum PasteType : int { enum PasteType : int {
// 0x0.st // 0x0.st
@ -58,32 +61,36 @@ class PasteUpload : public Task {
First = NullPointer, First = NullPointer,
Last = Mclogs Last = Mclogs
}; };
struct PasteTypeInfo { struct PasteTypeInfo {
const QString name; const QString name;
const QString defaultBase; const QString defaultBase;
const QString endpointPath; const QString endpointPath;
}; };
static std::array<PasteTypeInfo, 4> PasteTypes; static const std::array<PasteTypeInfo, 4> PasteTypes;
PasteUpload(QWidget* window, QString text, QString url, PasteType pasteType); class Sink : public Net::ByteArraySink {
virtual ~PasteUpload(); public:
Sink(PasteUpload* p) : Net::ByteArraySink(std::make_shared<QByteArray>()), m_d(p) {};
virtual ~Sink() = default;
public:
auto finalize(QNetworkReply& reply) -> Task::State override;
private:
PasteUpload* m_d;
};
friend Sink;
PasteUpload(const QString& log, QString url, PasteType pasteType);
virtual ~PasteUpload() = default;
QString pasteLink() { return m_pasteLink; } QString pasteLink() { return m_pasteLink; }
protected:
virtual void executeTask();
private: private:
QWidget* m_window; virtual QNetworkReply* getReply(QNetworkRequest&) override;
QString m_log;
QString m_pasteLink; QString m_pasteLink;
QString m_baseUrl; QString m_baseUrl;
QString m_uploadUrl; const PasteType m_paste_type;
PasteType m_pasteType;
QByteArray m_text;
std::shared_ptr<QNetworkReply> m_reply;
public slots:
void downloadError(QNetworkReply::NetworkError);
void downloadFinished();
}; };

View file

@ -52,6 +52,8 @@ class Sink {
virtual auto hasLocalData() -> bool = 0; virtual auto hasLocalData() -> bool = 0;
QString failReason() const { return m_fail_reason; }
void addValidator(Validator* validator) void addValidator(Validator* validator)
{ {
if (validator) { if (validator) {
@ -95,5 +97,6 @@ class Sink {
protected: protected:
std::vector<std::shared_ptr<Validator>> validators; std::vector<std::shared_ptr<Validator>> validators;
QString m_fail_reason;
}; };
} // namespace Net } // namespace Net

View file

@ -86,6 +86,7 @@ auto ImgurAlbumCreation::Sink::write(QByteArray& data) -> Task::State
auto ImgurAlbumCreation::Sink::abort() -> Task::State auto ImgurAlbumCreation::Sink::abort() -> Task::State
{ {
m_output.clear(); m_output.clear();
m_fail_reason = "Aborted";
return Task::State::Failed; return Task::State::Failed;
} }
@ -95,11 +96,13 @@ auto ImgurAlbumCreation::Sink::finalize(QNetworkReply&) -> Task::State
QJsonDocument doc = QJsonDocument::fromJson(m_output, &jsonError); QJsonDocument doc = QJsonDocument::fromJson(m_output, &jsonError);
if (jsonError.error != QJsonParseError::NoError) { if (jsonError.error != QJsonParseError::NoError) {
qDebug() << jsonError.errorString(); qDebug() << jsonError.errorString();
m_fail_reason = "Invalid json reply";
return Task::State::Failed; return Task::State::Failed;
} }
auto object = doc.object(); auto object = doc.object();
if (!object.value("success").toBool()) { if (!object.value("success").toBool()) {
qDebug() << doc.toJson(); qDebug() << doc.toJson();
m_fail_reason = "Failed to create album";
return Task::State::Failed; return Task::State::Failed;
} }
m_result->deleteHash = object.value("data").toObject().value("deletehash").toString(); m_result->deleteHash = object.value("data").toObject().value("deletehash").toString();

View file

@ -90,6 +90,7 @@ auto ImgurUpload::Sink::write(QByteArray& data) -> Task::State
auto ImgurUpload::Sink::abort() -> Task::State auto ImgurUpload::Sink::abort() -> Task::State
{ {
m_output.clear(); m_output.clear();
m_fail_reason = "Aborted";
return Task::State::Failed; return Task::State::Failed;
} }
@ -99,11 +100,13 @@ auto ImgurUpload::Sink::finalize(QNetworkReply&) -> Task::State
QJsonDocument doc = QJsonDocument::fromJson(m_output, &jsonError); QJsonDocument doc = QJsonDocument::fromJson(m_output, &jsonError);
if (jsonError.error != QJsonParseError::NoError) { if (jsonError.error != QJsonParseError::NoError) {
qDebug() << "imgur server did not reply with JSON" << jsonError.errorString(); qDebug() << "imgur server did not reply with JSON" << jsonError.errorString();
m_fail_reason = "Invalid json reply";
return Task::State::Failed; return Task::State::Failed;
} }
auto object = doc.object(); auto object = doc.object();
if (!object.value("success").toBool()) { if (!object.value("success").toBool()) {
qDebug() << "Screenshot upload not successful:" << doc.toJson(); qDebug() << "Screenshot upload not successful:" << doc.toJson();
m_fail_reason = "Screenshot was not uploaded successfully";
return Task::State::Failed; return Task::State::Failed;
} }
m_shot->m_imgurId = object.value("data").toObject().value("id").toString(); m_shot->m_imgurId = object.value("data").toObject().value("id").toString();

View file

@ -118,10 +118,29 @@ void ConcurrentTask::executeNextSubTask()
} }
if (m_queue.isEmpty()) { if (m_queue.isEmpty()) {
if (m_doing.isEmpty()) { if (m_doing.isEmpty()) {
if (m_failed.isEmpty()) if (m_failed.isEmpty()) {
emitSucceeded(); emitSucceeded();
else } else if (m_failed.count() == 1) {
emitFailed(tr("One or more subtasks failed")); auto task = m_failed.keys().first();
auto reason = task->failReason();
if (reason.isEmpty()) { // clearly a bug somewhere
reason = tr("Task failed");
}
emitFailed(reason);
} else {
QStringList failReason;
for (auto t : m_failed) {
auto reason = t->failReason();
if (!reason.isEmpty()) {
failReason << reason;
}
}
if (failReason.isEmpty()) {
emitFailed(tr("Multiple subtasks failed"));
} else {
emitFailed(tr("Multiple subtasks failed\n%1").arg(failReason.join("\n")));
}
}
} }
return; return;
} }

View file

@ -196,6 +196,8 @@ void Task::logWarning(const QString& line)
{ {
qWarning() << line; qWarning() << line;
m_Warnings.append(line); m_Warnings.append(line);
emit warningLogged(line);
} }
QStringList Task::warnings() const QStringList Task::warnings() const

View file

@ -79,7 +79,6 @@ Q_DECLARE_METATYPE(TaskStepProgress)
using TaskStepProgressList = QList<std::shared_ptr<TaskStepProgress>>; using TaskStepProgressList = QList<std::shared_ptr<TaskStepProgress>>;
/*! /*!
* Represents a task that has to be done. * Represents a task that has to be done.
* To create a task, you need to subclass this class, implement the executeTask() method and call * To create a task, you need to subclass this class, implement the executeTask() method and call
@ -146,6 +145,7 @@ class Task : public QObject, public QRunnable {
void failed(QString reason); void failed(QString reason);
void status(QString status); void status(QString status);
void details(QString details); void details(QString details);
void warningLogged(const QString& warning);
void stepProgress(TaskStepProgress const& task_progress); void stepProgress(TaskStepProgress const& task_progress);
//! Emitted when the canAbort() status has changed. */ //! Emitted when the canAbort() status has changed. */
@ -177,9 +177,9 @@ class Task : public QObject, public QRunnable {
virtual void executeTask() = 0; virtual void executeTask() = 0;
protected slots: protected slots:
//! The Task subclass must call this method when the task has succeeded //! The Task subclass must call this method when the task has succeeded
virtual void emitSucceeded(); virtual void emitSucceeded();
//! **The Task subclass** must call this method when the task has aborted. External code should call abort() instead. //! **The Task subclass** must call this method when the task has aborted. External code should call abort() instead.
virtual void emitAborted(); virtual void emitAborted();
//! The Task subclass must call this method when the task has failed //! The Task subclass must call this method when the task has failed
virtual void emitFailed(QString reason = ""); virtual void emitFailed(QString reason = "");

View file

@ -38,10 +38,15 @@
#include "GuiUtil.h" #include "GuiUtil.h"
#include <QApplication> #include <QApplication>
#include <QBuffer>
#include <QClipboard> #include <QClipboard>
#include <QFileDialog> #include <QFileDialog>
#include <QStandardPaths> #include <QStandardPaths>
#include "FileSystem.h"
#include "logs/AnonymizeLog.h"
#include "net/NetJob.h"
#include "net/NetRequest.h"
#include "net/PasteUpload.h" #include "net/PasteUpload.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ProgressDialog.h"
@ -74,52 +79,52 @@ QString truncateLogForMclogs(const QString& logContent)
return logContent; return logContent;
} }
std::optional<QString> GuiUtil::uploadPaste(const QString& name, const QFileInfo& filePath, QWidget* parentWidget)
{
return uploadPaste(name, FS::read(filePath.absoluteFilePath()), parentWidget);
};
std::optional<QString> GuiUtil::uploadPaste(const QString& name, const QString& text, QWidget* parentWidget) std::optional<QString> GuiUtil::uploadPaste(const QString& name, const QString& text, QWidget* parentWidget)
{ {
ProgressDialog dialog(parentWidget); ProgressDialog dialog(parentWidget);
auto pasteTypeSetting = static_cast<PasteUpload::PasteType>(APPLICATION->settings()->get("PastebinType").toInt()); auto pasteType = static_cast<PasteUpload::PasteType>(APPLICATION->settings()->get("PastebinType").toInt());
auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); auto baseURL = APPLICATION->settings()->get("PastebinCustomAPIBase").toString();
bool shouldTruncate = false; bool shouldTruncate = false;
{ if (baseURL.isEmpty())
QUrl baseUrl; baseURL = PasteUpload::PasteTypes[pasteType].defaultBase;
if (pasteCustomAPIBaseSetting.isEmpty())
baseUrl = PasteUpload::PasteTypes[pasteTypeSetting].defaultBase;
else
baseUrl = pasteCustomAPIBaseSetting;
if (baseUrl.isValid()) { if (auto url = QUrl(baseURL); url.isValid()) {
auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"), auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"),
QObject::tr("You are about to upload \"%1\" to %2.\n" QObject::tr("You are about to upload \"%1\" to %2.\n"
"You should double-check for personal information.\n\n" "You should double-check for personal information.\n\n"
"Are you sure?") "Are you sure?")
.arg(name, baseUrl.host()), .arg(name, url.host()),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec(); ->exec();
if (response != QMessageBox::Yes) if (response != QMessageBox::Yes)
return {};
if (baseURL == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) {
auto truncateResponse = CustomMessageBox::selectable(
parentWidget, QObject::tr("Confirm Truncation"),
QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n"
"The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n"
"If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off "
"potentially useful info like crashes at the end.\n\n"
"Proceed with truncation?")
.arg(text.count("\n"))
.arg(MaxMclogsLines)
.arg(InitialMclogsLines)
.arg(FinalMclogsLines),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No)
->exec();
if (truncateResponse == QMessageBox::Cancel) {
return {}; return {};
if (baseUrl.toString() == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) {
auto truncateResponse = CustomMessageBox::selectable(
parentWidget, QObject::tr("Confirm Truncation"),
QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n"
"The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n"
"If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off "
"potentially useful info like crashes at the end.\n\n"
"Proceed with truncation?")
.arg(text.count("\n"))
.arg(MaxMclogsLines)
.arg(InitialMclogsLines)
.arg(FinalMclogsLines),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No)
->exec();
if (truncateResponse == QMessageBox::Cancel) {
return {};
}
shouldTruncate = truncateResponse == QMessageBox::Yes;
} }
shouldTruncate = truncateResponse == QMessageBox::Yes;
} }
} }
@ -128,26 +133,40 @@ std::optional<QString> GuiUtil::uploadPaste(const QString& name, const QString&
textToUpload = truncateLogForMclogs(text); textToUpload = truncateLogForMclogs(text);
} }
std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, textToUpload, pasteCustomAPIBaseSetting, pasteTypeSetting)); auto job = NetJob::Ptr(new NetJob("Log Upload", APPLICATION->network()));
dialog.execWithTask(paste.get()); auto pasteJob = new PasteUpload(textToUpload, baseURL, pasteType);
if (!paste->wasSuccessful()) { job->addNetAction(Net::NetRequest::Ptr(pasteJob));
CustomMessageBox::selectable(parentWidget, QObject::tr("Upload failed"), paste->failReason(), QMessageBox::Critical)->exec(); QObject::connect(job.get(), &Task::failed, [parentWidget](QString reason) {
return QString(); CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), reason, QMessageBox::Critical)->show();
} else { });
const QString link = paste->pasteLink(); QObject::connect(job.get(), &Task::aborted, [parentWidget] {
setClipboardText(link); CustomMessageBox::selectable(parentWidget, QObject::tr("Logs upload aborted"),
QObject::tr("The task has been aborted by the user."), QMessageBox::Information)
->show();
});
if (dialog.execWithTask(job.get()) == QDialog::Accepted) {
if (pasteJob->pasteLink().isEmpty()) {
CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), "The upload link is empty",
QMessageBox::Critical)
->show();
return {};
}
setClipboardText(pasteJob->pasteLink());
CustomMessageBox::selectable( CustomMessageBox::selectable(
parentWidget, QObject::tr("Upload finished"), parentWidget, QObject::tr("Upload finished"),
QObject::tr("The <a href=\"%1\">link to the uploaded log</a> has been placed in your clipboard.").arg(link), QObject::tr("The <a href=\"%1\">link to the uploaded log</a> has been placed in your clipboard.").arg(pasteJob->pasteLink()),
QMessageBox::Information) QMessageBox::Information)
->exec(); ->exec();
return link; return pasteJob->pasteLink();
} }
return {};
} }
void GuiUtil::setClipboardText(const QString& text) void GuiUtil::setClipboardText(QString text)
{ {
anonymizeLog(text);
QApplication::clipboard()->setText(text); QApplication::clipboard()->setText(text);
} }

View file

@ -1,11 +1,13 @@
#pragma once #pragma once
#include <QFileInfo>
#include <QWidget> #include <QWidget>
#include <optional> #include <optional>
namespace GuiUtil { namespace GuiUtil {
std::optional<QString> uploadPaste(const QString& name, const QString& text, QWidget* parentWidget); std::optional<QString> uploadPaste(const QString& name, const QFileInfo& filePath, QWidget* parentWidget);
void setClipboardText(const QString& text); std::optional<QString> uploadPaste(const QString& name, const QString& data, QWidget* parentWidget);
void setClipboardText(QString text);
QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget); QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget);
QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget); QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget);
} // namespace GuiUtil } // namespace GuiUtil

View file

@ -71,6 +71,7 @@
#include <QToolButton> #include <QToolButton>
#include <QWidget> #include <QWidget>
#include <QWidgetAction> #include <QWidgetAction>
#include <memory>
#include <BaseInstance.h> #include <BaseInstance.h>
#include <BuildConfig.h> #include <BuildConfig.h>
@ -90,8 +91,10 @@
#include <updater/ExternalUpdater.h> #include <updater/ExternalUpdater.h>
#include "InstanceWindow.h" #include "InstanceWindow.h"
#include "ui/GuiUtil.h"
#include "ui/dialogs/AboutDialog.h" #include "ui/dialogs/AboutDialog.h"
#include "ui/dialogs/CopyInstanceDialog.h" #include "ui/dialogs/CopyInstanceDialog.h"
#include "ui/dialogs/CreateShortcutDialog.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ExportInstanceDialog.h" #include "ui/dialogs/ExportInstanceDialog.h"
#include "ui/dialogs/ExportPackDialog.h" #include "ui/dialogs/ExportPackDialog.h"
@ -235,6 +238,16 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi
ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED); ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED);
} }
{ // logs upload
auto menu = new QMenu(this);
for (auto file : QDir("logs").entryInfoList(QDir::Files)) {
auto action = menu->addAction(file.fileName());
connect(action, &QAction::triggered, this, [this, file] { GuiUtil::uploadPaste(file.fileName(), file, this); });
}
ui->actionUploadLog->setMenu(menu);
}
// add the toolbar toggles to the view menu // add the toolbar toggles to the view menu
ui->viewMenu->addAction(ui->instanceToolBar->toggleViewAction()); ui->viewMenu->addAction(ui->instanceToolBar->toggleViewAction());
ui->viewMenu->addAction(ui->newsToolBar->toggleViewAction()); ui->viewMenu->addAction(ui->newsToolBar->toggleViewAction());
@ -1311,7 +1324,7 @@ void MainWindow::on_actionReportBug_triggered()
void MainWindow::on_actionClearMetadata_triggered() void MainWindow::on_actionClearMetadata_triggered()
{ {
//This if contains side effects! // This if contains side effects!
if (!APPLICATION->metacache()->evictAll()) { if (!APPLICATION->metacache()->evictAll()) {
CustomMessageBox::selectable(this, tr("Error"), CustomMessageBox::selectable(this, tr("Error"),
tr("Metadata cache clear Failed!\nTo clear the metadata cache manually, press Folders -> View " tr("Metadata cache clear Failed!\nTo clear the metadata cache manually, press Folders -> View "
@ -1383,6 +1396,14 @@ void MainWindow::on_actionDeleteInstance_triggered()
return; return;
} }
if (m_selectedInstance->isRunning()) {
CustomMessageBox::selectable(this, tr("Cannot Delete Running Instance"),
tr("The selected instance is currently running and cannot be deleted. Please stop the instance before "
"attempting to delete it."),
QMessageBox::Warning, QMessageBox::Ok)
->exec();
return;
}
auto id = m_selectedInstance->id(); auto id = m_selectedInstance->id();
auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"),
@ -1419,15 +1440,18 @@ void MainWindow::on_actionExportInstanceZip_triggered()
void MainWindow::on_actionExportInstanceMrPack_triggered() void MainWindow::on_actionExportInstanceMrPack_triggered()
{ {
if (m_selectedInstance) { if (m_selectedInstance) {
ExportPackDialog dlg(m_selectedInstance, this); auto instance = std::dynamic_pointer_cast<MinecraftInstance>(m_selectedInstance);
dlg.exec(); if (instance != nullptr) {
ExportPackDialog dlg(instance, this);
dlg.exec();
}
} }
} }
void MainWindow::on_actionExportInstanceFlamePack_triggered() void MainWindow::on_actionExportInstanceFlamePack_triggered()
{ {
if (m_selectedInstance) { if (m_selectedInstance) {
auto instance = dynamic_cast<MinecraftInstance*>(m_selectedInstance.get()); auto instance = std::dynamic_pointer_cast<MinecraftInstance>(m_selectedInstance);
if (instance) { if (instance) {
if (auto cmp = instance->getPackProfile()->getComponent("net.minecraft"); if (auto cmp = instance->getPackProfile()->getComponent("net.minecraft");
cmp && cmp->getVersionFile() && cmp->getVersionFile()->type == "snapshot") { cmp && cmp->getVersionFile() && cmp->getVersionFile()->type == "snapshot") {
@ -1436,7 +1460,7 @@ void MainWindow::on_actionExportInstanceFlamePack_triggered()
msgBox.exec(); msgBox.exec();
return; return;
} }
ExportPackDialog dlg(m_selectedInstance, this, ModPlatform::ResourceProvider::FLAME); ExportPackDialog dlg(instance, this, ModPlatform::ResourceProvider::FLAME);
dlg.exec(); dlg.exec();
} }
} }
@ -1510,139 +1534,11 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered()
{ {
if (!m_selectedInstance) if (!m_selectedInstance)
return; return;
auto desktopPath = FS::getDesktopDir();
if (desktopPath.isEmpty()) { CreateShortcutDialog shortcutDlg(m_selectedInstance, this);
// TODO come up with an alternative solution (open "save file" dialog) if (!shortcutDlg.exec())
QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!"));
return; return;
} shortcutDlg.createShortcut();
QString desktopFilePath;
QString appPath = QApplication::applicationFilePath();
QString iconPath;
QStringList args;
#if defined(Q_OS_MACOS)
appPath = QApplication::applicationFilePath();
if (appPath.startsWith("/private/var/")) {
QMessageBox::critical(this, tr("Create instance shortcut"),
tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts."));
return;
}
auto pIcon = APPLICATION->icons()->icon(m_selectedInstance->iconKey());
if (pIcon == nullptr) {
pIcon = APPLICATION->icons()->icon("grass");
}
iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "Icon.icns");
QFile iconFile(iconPath);
if (!iconFile.open(QFile::WriteOnly)) {
QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application."));
return;
}
QIcon icon = pIcon->icon();
bool success = icon.pixmap(1024, 1024).save(iconPath, "ICNS");
iconFile.close();
if (!success) {
iconFile.remove();
QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application."));
return;
}
#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
if (appPath.startsWith("/tmp/.mount_")) {
// AppImage!
appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE"));
if (appPath.isEmpty()) {
QMessageBox::critical(this, tr("Create instance shortcut"),
tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)"));
} else if (appPath.endsWith("/")) {
appPath.chop(1);
}
}
auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey());
if (icon == nullptr) {
icon = APPLICATION->icons()->icon("grass");
}
iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png");
QFile iconFile(iconPath);
if (!iconFile.open(QFile::WriteOnly)) {
QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
return;
}
bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG");
iconFile.close();
if (!success) {
iconFile.remove();
QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
return;
}
if (DesktopServices::isFlatpak()) {
desktopFilePath = FS::PathCombine(desktopPath, FS::RemoveInvalidFilenameChars(m_selectedInstance->name()) + ".desktop");
QFileDialog fileDialog;
// workaround to make sure the portal file dialog opens in the desktop directory
fileDialog.setDirectoryUrl(desktopPath);
desktopFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), desktopFilePath, tr("Desktop Entries") + " (*.desktop)");
if (desktopFilePath.isEmpty())
return; // file dialog canceled by user
appPath = "flatpak";
args.append({ "run", BuildConfig.LAUNCHER_APPID });
}
#elif defined(Q_OS_WIN)
auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey());
if (icon == nullptr) {
icon = APPLICATION->icons()->icon("grass");
}
iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico");
// part of fix for weird bug involving the window icon being replaced
// dunno why it happens, but this 2-line fix seems to be enough, so w/e
auto appIcon = APPLICATION->getThemedIcon("logo");
QFile iconFile(iconPath);
if (!iconFile.open(QFile::WriteOnly)) {
QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
return;
}
bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO");
iconFile.close();
// restore original window icon
QGuiApplication::setWindowIcon(appIcon);
if (!success) {
iconFile.remove();
QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut."));
return;
}
#else
QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!"));
return;
#endif
args.append({ "--launch", m_selectedInstance->id() });
if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) {
#if not defined(Q_OS_MACOS)
QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!"));
#else
QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!"));
#endif
} else {
#if not defined(Q_OS_MACOS)
iconFile.remove();
#endif
QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!"));
}
} }
void MainWindow::taskEnd() void MainWindow::taskEnd()

View file

@ -131,7 +131,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>800</width> <width>800</width>
<height>27</height> <height>22</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="fileMenu"> <widget class="QMenu" name="fileMenu">
@ -215,6 +215,7 @@
</property> </property>
<addaction name="actionClearMetadata"/> <addaction name="actionClearMetadata"/>
<addaction name="actionReportBug"/> <addaction name="actionReportBug"/>
<addaction name="actionUploadLog"/>
<addaction name="actionAddToPATH"/> <addaction name="actionAddToPATH"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionMATRIX"/> <addaction name="actionMATRIX"/>
@ -235,8 +236,7 @@
</widget> </widget>
<action name="actionMoreNews"> <action name="actionMoreNews">
<property name="icon"> <property name="icon">
<iconset theme="news"> <iconset theme="news"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>More news...</string> <string>More news...</string>
@ -250,8 +250,7 @@
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="icon"> <property name="icon">
<iconset theme="cat"> <iconset theme="cat"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Meow</string> <string>&amp;Meow</string>
@ -286,8 +285,7 @@
</action> </action>
<action name="actionAddInstance"> <action name="actionAddInstance">
<property name="icon"> <property name="icon">
<iconset theme="new"> <iconset theme="new"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Add Instanc&amp;e...</string> <string>Add Instanc&amp;e...</string>
@ -298,8 +296,7 @@
</action> </action>
<action name="actionCheckUpdate"> <action name="actionCheckUpdate">
<property name="icon"> <property name="icon">
<iconset theme="checkupdate"> <iconset theme="checkupdate"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Update...</string> <string>&amp;Update...</string>
@ -313,8 +310,7 @@
</action> </action>
<action name="actionSettings"> <action name="actionSettings">
<property name="icon"> <property name="icon">
<iconset theme="settings"> <iconset theme="settings"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Setti&amp;ngs...</string> <string>Setti&amp;ngs...</string>
@ -328,8 +324,7 @@
</action> </action>
<action name="actionManageAccounts"> <action name="actionManageAccounts">
<property name="icon"> <property name="icon">
<iconset theme="accounts"> <iconset theme="accounts"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Manage Accounts...</string> <string>&amp;Manage Accounts...</string>
@ -337,8 +332,7 @@
</action> </action>
<action name="actionLaunchInstance"> <action name="actionLaunchInstance">
<property name="icon"> <property name="icon">
<iconset theme="launch"> <iconset theme="launch"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Launch</string> <string>&amp;Launch</string>
@ -349,8 +343,7 @@
</action> </action>
<action name="actionKillInstance"> <action name="actionKillInstance">
<property name="icon"> <property name="icon">
<iconset theme="status-bad"> <iconset theme="status-bad"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Kill</string> <string>&amp;Kill</string>
@ -364,8 +357,7 @@
</action> </action>
<action name="actionRenameInstance"> <action name="actionRenameInstance">
<property name="icon"> <property name="icon">
<iconset theme="rename"> <iconset theme="rename"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Rename</string> <string>Rename</string>
@ -376,8 +368,7 @@
</action> </action>
<action name="actionChangeInstGroup"> <action name="actionChangeInstGroup">
<property name="icon"> <property name="icon">
<iconset theme="tag"> <iconset theme="tag"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Change Group...</string> <string>&amp;Change Group...</string>
@ -399,8 +390,7 @@
</action> </action>
<action name="actionEditInstance"> <action name="actionEditInstance">
<property name="icon"> <property name="icon">
<iconset theme="settings"> <iconset theme="settings"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Edit...</string> <string>&amp;Edit...</string>
@ -414,8 +404,7 @@
</action> </action>
<action name="actionViewSelectedInstFolder"> <action name="actionViewSelectedInstFolder">
<property name="icon"> <property name="icon">
<iconset theme="viewfolder"> <iconset theme="viewfolder"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Folder</string> <string>&amp;Folder</string>
@ -426,8 +415,7 @@
</action> </action>
<action name="actionDeleteInstance"> <action name="actionDeleteInstance">
<property name="icon"> <property name="icon">
<iconset theme="delete"> <iconset theme="delete"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Dele&amp;te</string> <string>Dele&amp;te</string>
@ -441,8 +429,7 @@
</action> </action>
<action name="actionCopyInstance"> <action name="actionCopyInstance">
<property name="icon"> <property name="icon">
<iconset theme="copy"> <iconset theme="copy"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Cop&amp;y...</string> <string>Cop&amp;y...</string>
@ -456,8 +443,7 @@
</action> </action>
<action name="actionExportInstance"> <action name="actionExportInstance">
<property name="icon"> <property name="icon">
<iconset theme="export"> <iconset theme="export"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>E&amp;xport...</string> <string>E&amp;xport...</string>
@ -468,8 +454,7 @@
</action> </action>
<action name="actionExportInstanceZip"> <action name="actionExportInstanceZip">
<property name="icon"> <property name="icon">
<iconset theme="launcher"> <iconset theme="launcher"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Prism Launcher (zip)</string> <string>Prism Launcher (zip)</string>
@ -477,8 +462,7 @@
</action> </action>
<action name="actionExportInstanceMrPack"> <action name="actionExportInstanceMrPack">
<property name="icon"> <property name="icon">
<iconset theme="modrinth"> <iconset theme="modrinth"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Modrinth (mrpack)</string> <string>Modrinth (mrpack)</string>
@ -486,8 +470,7 @@
</action> </action>
<action name="actionExportInstanceFlamePack"> <action name="actionExportInstanceFlamePack">
<property name="icon"> <property name="icon">
<iconset theme="flame"> <iconset theme="flame"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>CurseForge (zip)</string> <string>CurseForge (zip)</string>
@ -495,20 +478,18 @@
</action> </action>
<action name="actionCreateInstanceShortcut"> <action name="actionCreateInstanceShortcut">
<property name="icon"> <property name="icon">
<iconset theme="shortcut"> <iconset theme="shortcut"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Create Shortcut</string> <string>Create Shortcut</string>
</property> </property>
<property name="toolTip"> <property name="toolTip">
<string>Creates a shortcut on your desktop to launch the selected instance.</string> <string>Creates a shortcut on a selected folder to launch the selected instance.</string>
</property> </property>
</action> </action>
<action name="actionNoAccountsAdded"> <action name="actionNoAccountsAdded">
<property name="icon"> <property name="icon">
<iconset theme="noaccount"> <iconset theme="noaccount"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>No accounts added!</string> <string>No accounts added!</string>
@ -519,8 +500,7 @@
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="icon"> <property name="icon">
<iconset theme="noaccount"> <iconset theme="noaccount"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>No Default Account</string> <string>No Default Account</string>
@ -531,8 +511,7 @@
</action> </action>
<action name="actionCloseWindow"> <action name="actionCloseWindow">
<property name="icon"> <property name="icon">
<iconset theme="status-bad"> <iconset theme="status-bad"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Close &amp;Window</string> <string>Close &amp;Window</string>
@ -546,8 +525,7 @@
</action> </action>
<action name="actionViewInstanceFolder"> <action name="actionViewInstanceFolder">
<property name="icon"> <property name="icon">
<iconset theme="viewfolder"> <iconset theme="viewfolder"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Instances</string> <string>&amp;Instances</string>
@ -558,8 +536,7 @@
</action> </action>
<action name="actionViewLauncherRootFolder"> <action name="actionViewLauncherRootFolder">
<property name="icon"> <property name="icon">
<iconset theme="viewfolder"> <iconset theme="viewfolder"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Launcher &amp;Root</string> <string>Launcher &amp;Root</string>
@ -570,8 +547,7 @@
</action> </action>
<action name="actionViewCentralModsFolder"> <action name="actionViewCentralModsFolder">
<property name="icon"> <property name="icon">
<iconset theme="centralmods"> <iconset theme="centralmods"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Central Mods</string> <string>&amp;Central Mods</string>
@ -582,8 +558,7 @@
</action> </action>
<action name="actionViewSkinsFolder"> <action name="actionViewSkinsFolder">
<property name="icon"> <property name="icon">
<iconset theme="viewfolder"> <iconset theme="viewfolder"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Skins</string> <string>&amp;Skins</string>
@ -594,8 +569,7 @@
</action> </action>
<action name="actionViewIconsFolder"> <action name="actionViewIconsFolder">
<property name="icon"> <property name="icon">
<iconset theme="viewfolder"> <iconset theme="viewfolder"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Instance Icons</string> <string>Instance Icons</string>
@ -606,8 +580,7 @@
</action> </action>
<action name="actionViewLogsFolder"> <action name="actionViewLogsFolder">
<property name="icon"> <property name="icon">
<iconset theme="viewfolder"> <iconset theme="viewfolder"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Logs</string> <string>Logs</string>
@ -623,8 +596,7 @@
</action> </action>
<action name="actionReportBug"> <action name="actionReportBug">
<property name="icon"> <property name="icon">
<iconset theme="bug"> <iconset theme="bug"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Report a Bug or Suggest a Feature</string> <string>Report a Bug or Suggest a Feature</string>
@ -635,8 +607,7 @@
</action> </action>
<action name="actionDISCORD"> <action name="actionDISCORD">
<property name="icon"> <property name="icon">
<iconset theme="discord"> <iconset theme="discord"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Discord Guild</string> <string>&amp;Discord Guild</string>
@ -647,8 +618,7 @@
</action> </action>
<action name="actionMATRIX"> <action name="actionMATRIX">
<property name="icon"> <property name="icon">
<iconset theme="matrix"> <iconset theme="matrix"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Matrix Space</string> <string>&amp;Matrix Space</string>
@ -659,8 +629,7 @@
</action> </action>
<action name="actionREDDIT"> <action name="actionREDDIT">
<property name="icon"> <property name="icon">
<iconset theme="reddit-alien"> <iconset theme="reddit-alien"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Sub&amp;reddit</string> <string>Sub&amp;reddit</string>
@ -671,8 +640,7 @@
</action> </action>
<action name="actionAbout"> <action name="actionAbout">
<property name="icon"> <property name="icon">
<iconset theme="about"> <iconset theme="about"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;About %1</string> <string>&amp;About %1</string>
@ -686,8 +654,7 @@
</action> </action>
<action name="actionClearMetadata"> <action name="actionClearMetadata">
<property name="icon"> <property name="icon">
<iconset theme="refresh"> <iconset theme="refresh"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Clear Metadata Cache</string> <string>&amp;Clear Metadata Cache</string>
@ -696,10 +663,21 @@
<string>Clear cached metadata</string> <string>Clear cached metadata</string>
</property> </property>
</action> </action>
<action name="actionUploadLog">
<property name="icon">
<iconset theme="log">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Upload logs</string>
</property>
<property name="toolTip">
<string>Upload launcher logs to the selected log provider</string>
</property>
</action>
<action name="actionAddToPATH"> <action name="actionAddToPATH">
<property name="icon"> <property name="icon">
<iconset theme="custom-commands"> <iconset theme="custom-commands"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Install to &amp;PATH</string> <string>Install to &amp;PATH</string>
@ -710,8 +688,7 @@
</action> </action>
<action name="actionFoldersButton"> <action name="actionFoldersButton">
<property name="icon"> <property name="icon">
<iconset theme="viewfolder"> <iconset theme="viewfolder"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Folders</string> <string>Folders</string>
@ -722,8 +699,7 @@
</action> </action>
<action name="actionHelpButton"> <action name="actionHelpButton">
<property name="icon"> <property name="icon">
<iconset theme="help"> <iconset theme="help"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Help</string> <string>Help</string>
@ -734,8 +710,7 @@
</action> </action>
<action name="actionAccountsButton"> <action name="actionAccountsButton">
<property name="icon"> <property name="icon">
<iconset theme="noaccount"> <iconset theme="noaccount"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Accounts</string> <string>Accounts</string>
@ -743,8 +718,7 @@
</action> </action>
<action name="actionOpenWiki"> <action name="actionOpenWiki">
<property name="icon"> <property name="icon">
<iconset theme="help"> <iconset theme="help"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>%1 &amp;Help</string> <string>%1 &amp;Help</string>
@ -755,8 +729,7 @@
</action> </action>
<action name="actionViewWidgetThemeFolder"> <action name="actionViewWidgetThemeFolder">
<property name="icon"> <property name="icon">
<iconset theme="viewfolder"> <iconset theme="viewfolder"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>&amp;Widget Themes</string> <string>&amp;Widget Themes</string>
@ -767,8 +740,7 @@
</action> </action>
<action name="actionViewIconThemeFolder"> <action name="actionViewIconThemeFolder">
<property name="icon"> <property name="icon">
<iconset theme="viewfolder"> <iconset theme="viewfolder"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>I&amp;con Theme</string> <string>I&amp;con Theme</string>
@ -779,8 +751,7 @@
</action> </action>
<action name="actionViewCatPackFolder"> <action name="actionViewCatPackFolder">
<property name="icon"> <property name="icon">
<iconset theme="viewfolder"> <iconset theme="viewfolder"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Cat Packs</string> <string>Cat Packs</string>
@ -791,8 +762,7 @@
</action> </action>
<action name="actionViewJavaFolder"> <action name="actionViewJavaFolder">
<property name="icon"> <property name="icon">
<iconset theme="viewfolder"> <iconset theme="viewfolder"/>
<normaloff>.</normaloff>.</iconset>
</property> </property>
<property name="text"> <property name="text">
<string>Java</string> <string>Java</string>

View file

@ -0,0 +1,223 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (C) 2025 Yihe Li <winmikedows@hotmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <QLayout>
#include <QPushButton>
#include "Application.h"
#include "BuildConfig.h"
#include "CreateShortcutDialog.h"
#include "ui_CreateShortcutDialog.h"
#include "ui/dialogs/IconPickerDialog.h"
#include "BaseInstance.h"
#include "DesktopServices.h"
#include "FileSystem.h"
#include "InstanceList.h"
#include "icons/IconList.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/ShortcutUtils.h"
#include "minecraft/WorldList.h"
#include "minecraft/auth/AccountList.h"
CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent)
: QDialog(parent), ui(new Ui::CreateShortcutDialog), m_instance(instance)
{
ui->setupUi(this);
InstIconKey = instance->iconKey();
ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
ui->instNameTextBox->setPlaceholderText(instance->name());
auto mInst = std::dynamic_pointer_cast<MinecraftInstance>(instance);
m_QuickJoinSupported = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer");
auto worldList = mInst->worldList();
worldList->update();
if (!m_QuickJoinSupported || worldList->empty()) {
ui->worldTarget->hide();
ui->worldSelectionBox->hide();
ui->serverTarget->setChecked(true);
ui->serverTarget->hide();
ui->serverLabel->show();
}
// Populate save targets
if (!DesktopServices::isFlatpak()) {
QString desktopDir = FS::getDesktopDir();
QString applicationDir = FS::getApplicationsDir();
if (!desktopDir.isEmpty())
ui->saveTargetSelectionBox->addItem(tr("Desktop"), QVariant::fromValue(SaveTarget::Desktop));
if (!applicationDir.isEmpty())
ui->saveTargetSelectionBox->addItem(tr("Applications"), QVariant::fromValue(SaveTarget::Applications));
}
ui->saveTargetSelectionBox->addItem(tr("Other..."), QVariant::fromValue(SaveTarget::Other));
// Populate worlds
if (m_QuickJoinSupported) {
for (const auto& world : worldList->allWorlds()) {
// Entry name: World Name [Game Mode] - Last Played: DateTime
QString entry_name = tr("%1 [%2] - Last Played: %3")
.arg(world.name(), world.gameType().toTranslatedString(), world.lastPlayed().toString(Qt::ISODate));
ui->worldSelectionBox->addItem(entry_name, world.name());
}
}
// Populate accounts
auto accounts = APPLICATION->accounts();
MinecraftAccountPtr defaultAccount = accounts->defaultAccount();
if (accounts->count() <= 0) {
ui->overrideAccountCheckbox->setEnabled(false);
} else {
for (int i = 0; i < accounts->count(); i++) {
MinecraftAccountPtr account = accounts->at(i);
auto profileLabel = account->profileName();
if (account->isInUse())
profileLabel = tr("%1 (in use)").arg(profileLabel);
auto face = account->getFace();
QIcon icon = face.isNull() ? APPLICATION->getThemedIcon("noaccount") : face;
ui->accountSelectionBox->addItem(profileLabel, account->profileName());
ui->accountSelectionBox->setItemIcon(i, icon);
if (defaultAccount == account)
ui->accountSelectionBox->setCurrentIndex(i);
}
}
}
CreateShortcutDialog::~CreateShortcutDialog()
{
delete ui;
}
void CreateShortcutDialog::on_iconButton_clicked()
{
IconPickerDialog dlg(this);
dlg.execWithSelection(InstIconKey);
if (dlg.result() == QDialog::Accepted) {
InstIconKey = dlg.selectedIconKey;
ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
}
}
void CreateShortcutDialog::on_overrideAccountCheckbox_stateChanged(int state)
{
ui->accountOptionsGroup->setEnabled(state == Qt::Checked);
}
void CreateShortcutDialog::on_targetCheckbox_stateChanged(int state)
{
ui->targetOptionsGroup->setEnabled(state == Qt::Checked);
ui->worldSelectionBox->setEnabled(ui->worldTarget->isChecked());
ui->serverAddressBox->setEnabled(ui->serverTarget->isChecked());
stateChanged();
}
void CreateShortcutDialog::on_worldTarget_toggled(bool checked)
{
ui->worldSelectionBox->setEnabled(checked);
stateChanged();
}
void CreateShortcutDialog::on_serverTarget_toggled(bool checked)
{
ui->serverAddressBox->setEnabled(checked);
stateChanged();
}
void CreateShortcutDialog::on_worldSelectionBox_currentIndexChanged(int index)
{
stateChanged();
}
void CreateShortcutDialog::on_serverAddressBox_textChanged(const QString& text)
{
stateChanged();
}
void CreateShortcutDialog::stateChanged()
{
QString result = m_instance->name();
if (ui->targetCheckbox->isChecked()) {
if (ui->worldTarget->isChecked())
result = tr("%1 - %2").arg(result, ui->worldSelectionBox->currentData().toString());
else if (ui->serverTarget->isChecked())
result = tr("%1 - Server %2").arg(result, ui->serverAddressBox->text());
}
ui->instNameTextBox->setPlaceholderText(result);
if (!ui->targetCheckbox->isChecked())
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
else {
ui->buttonBox->button(QDialogButtonBox::Ok)
->setEnabled((ui->worldTarget->isChecked() && ui->worldSelectionBox->currentIndex() != -1) ||
(ui->serverTarget->isChecked() && !ui->serverAddressBox->text().isEmpty()));
}
}
// Real work
void CreateShortcutDialog::createShortcut()
{
QString targetString = tr("instance");
QStringList extraArgs;
if (ui->targetCheckbox->isChecked()) {
if (ui->worldTarget->isChecked()) {
targetString = tr("world");
extraArgs = { "--world", ui->worldSelectionBox->currentData().toString() };
} else if (ui->serverTarget->isChecked()) {
targetString = tr("server");
extraArgs = { "--server", ui->serverAddressBox->text() };
}
}
auto target = ui->saveTargetSelectionBox->currentData().value<SaveTarget>();
auto name = ui->instNameTextBox->text();
if (name.isEmpty())
name = ui->instNameTextBox->placeholderText();
if (ui->overrideAccountCheckbox->isChecked())
extraArgs.append({ "--profile", ui->accountSelectionBox->currentData().toString() });
ShortcutUtils::Shortcut args{ m_instance.get(), name, targetString, this, extraArgs, InstIconKey };
if (target == SaveTarget::Desktop)
ShortcutUtils::createInstanceShortcutOnDesktop(args);
else if (target == SaveTarget::Applications)
ShortcutUtils::createInstanceShortcutInApplications(args);
else
ShortcutUtils::createInstanceShortcutInOther(args);
}

View file

@ -0,0 +1,62 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QDialog>
#include "BaseInstance.h"
class BaseInstance;
namespace Ui {
class CreateShortcutDialog;
}
class CreateShortcutDialog : public QDialog {
Q_OBJECT
public:
explicit CreateShortcutDialog(InstancePtr instance, QWidget* parent = nullptr);
~CreateShortcutDialog();
void createShortcut();
private slots:
// Icon, target and name
void on_iconButton_clicked();
// Override account
void on_overrideAccountCheckbox_stateChanged(int state);
// Override target (world, server)
void on_targetCheckbox_stateChanged(int state);
void on_worldTarget_toggled(bool checked);
void on_serverTarget_toggled(bool checked);
void on_worldSelectionBox_currentIndexChanged(int index);
void on_serverAddressBox_textChanged(const QString& text);
private:
// Data
Ui::CreateShortcutDialog* ui;
QString InstIconKey;
InstancePtr m_instance;
bool m_QuickJoinSupported = false;
// Index representations
enum class SaveTarget { Desktop, Applications, Other };
// Functions
void stateChanged();
};

View file

@ -0,0 +1,250 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CreateShortcutDialog</class>
<widget class="QDialog" name="CreateShortcutDialog">
<property name="windowModality">
<enum>Qt::WindowModality::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>450</width>
<height>370</height>
</rect>
</property>
<property name="windowTitle">
<string>Create Instance Shortcut</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="iconBtnLayout">
<item>
<widget class="QToolButton" name="iconButton">
<property name="icon">
<iconset>
<normaloff>:/icons/instances/grass</normaloff>:/icons/instances/grass</iconset>
</property>
<property name="iconSize">
<size>
<width>80</width>
<height>80</height>
</size>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="iconBtnGridLayout">
<item row="0" column="0">
<widget class="QLabel" name="saveToLabel">
<property name="text">
<string>Save To:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="saveTargetSelectionBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="instNameTextBox">
<property name="placeholderText">
<string>Name</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="overrideAccountCheckbox">
<property name="toolTip">
<string>Use a different account than the default specified.</string>
</property>
<property name="text">
<string>Override the default account</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="accountOptionsGroup">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="accountOptionsLayout">
<item>
<widget class="QComboBox" name="accountSelectionBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QCheckBox" name="targetCheckbox">
<property name="toolTip">
<string>Specify a world or server to automatically join on launch.</string>
</property>
<property name="text">
<string>Select a target to join on launch</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="targetOptionsGroup">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QGridLayout" name="targetOptionsGridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout" name="worldOverlap">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QRadioButton" name="worldTarget">
<property name="text">
<string>World:</string>
</property>
<attribute name="buttonGroup">
<string notr="true">targetBtnGroup</string>
</attribute>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="worldSelectionBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QVBoxLayout" name="serverOverlap">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QRadioButton" name="serverTarget">
<property name="text">
<string>Server Address:</string>
</property>
<attribute name="buttonGroup">
<string notr="true">targetBtnGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QLabel" name="serverLabel">
<property name="visible">
<bool>false</bool>
</property>
<property name="text">
<string>Server Address:</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="serverAddressBox">
<property name="placeholderText">
<string>Server Address</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>iconButton</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CreateShortcutDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CreateShortcutDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
</connections>
<buttongroups>
<buttongroup name="targetBtnGroup"/>
</buttongroups>
</ui>

View file

@ -17,7 +17,7 @@
*/ */
#include "ExportPackDialog.h" #include "ExportPackDialog.h"
#include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ResourceFolderModel.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "modplatform/flame/FlamePackExportTask.h" #include "modplatform/flame/FlamePackExportTask.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
@ -33,7 +33,7 @@
#include "MMCZip.h" #include "MMCZip.h"
#include "modplatform/modrinth/ModrinthPackExportTask.h" #include "modplatform/modrinth/ModrinthPackExportTask.h"
ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider) ExportPackDialog::ExportPackDialog(MinecraftInstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider)
: QDialog(parent), m_instance(instance), m_ui(new Ui::ExportPackDialog), m_provider(provider) : QDialog(parent), m_instance(instance), m_ui(new Ui::ExportPackDialog), m_provider(provider)
{ {
Q_ASSERT(m_provider == ModPlatform::ResourceProvider::MODRINTH || m_provider == ModPlatform::ResourceProvider::FLAME); Q_ASSERT(m_provider == ModPlatform::ResourceProvider::MODRINTH || m_provider == ModPlatform::ResourceProvider::FLAME);
@ -44,12 +44,16 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla
m_ui->version->setText(instance->settings()->get("ExportVersion").toString()); m_ui->version->setText(instance->settings()->get("ExportVersion").toString());
m_ui->optionalFiles->setChecked(instance->settings()->get("ExportOptionalFiles").toBool()); m_ui->optionalFiles->setChecked(instance->settings()->get("ExportOptionalFiles").toBool());
connect(m_ui->recommendedMemoryCheckBox, &QCheckBox::toggled, m_ui->recommendedMemory, &QWidget::setEnabled);
if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { if (m_provider == ModPlatform::ResourceProvider::MODRINTH) {
setWindowTitle(tr("Export Modrinth Pack")); setWindowTitle(tr("Export Modrinth Pack"));
m_ui->authorLabel->hide(); m_ui->authorLabel->hide();
m_ui->author->hide(); m_ui->author->hide();
m_ui->recommendedMemoryWidget->hide();
m_ui->summary->setPlainText(instance->settings()->get("ExportSummary").toString()); m_ui->summary->setPlainText(instance->settings()->get("ExportSummary").toString());
} else { } else {
setWindowTitle(tr("Export CurseForge Pack")); setWindowTitle(tr("Export CurseForge Pack"));
@ -57,6 +61,19 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla
m_ui->summaryLabel->hide(); m_ui->summaryLabel->hide();
m_ui->summary->hide(); m_ui->summary->hide();
const int recommendedRAM = instance->settings()->get("ExportRecommendedRAM").toInt();
if (recommendedRAM > 0) {
m_ui->recommendedMemoryCheckBox->setChecked(true);
m_ui->recommendedMemory->setValue(recommendedRAM);
} else {
m_ui->recommendedMemoryCheckBox->setChecked(false);
// recommend based on setting - limited to 12 GiB (CurseForge warns above this amount)
const int defaultRecommendation = qMin(m_instance->settings()->get("MaxMemAlloc").toInt(), 1024 * 12);
m_ui->recommendedMemory->setValue(defaultRecommendation);
}
m_ui->author->setText(instance->settings()->get("ExportAuthor").toString()); m_ui->author->setText(instance->settings()->get("ExportAuthor").toString());
} }
@ -120,9 +137,15 @@ void ExportPackDialog::done(int result)
if (m_provider == ModPlatform::ResourceProvider::MODRINTH) if (m_provider == ModPlatform::ResourceProvider::MODRINTH)
settings->set("ExportSummary", m_ui->summary->toPlainText()); settings->set("ExportSummary", m_ui->summary->toPlainText());
else else {
settings->set("ExportAuthor", m_ui->author->text()); settings->set("ExportAuthor", m_ui->author->text());
if (m_ui->recommendedMemoryCheckBox->isChecked())
settings->set("ExportRecommendedRAM", m_ui->recommendedMemory->value());
else
settings->reset("ExportRecommendedRAM");
}
if (result == Accepted) { if (result == Accepted) {
const QString name = m_ui->name->text().isEmpty() ? m_instance->name() : m_ui->name->text(); const QString name = m_ui->name->text().isEmpty() ? m_instance->name() : m_ui->name->text();
const QString filename = FS::RemoveInvalidFilenameChars(name); const QString filename = FS::RemoveInvalidFilenameChars(name);
@ -149,8 +172,18 @@ void ExportPackDialog::done(int result)
task = new ModrinthPackExportTask(name, m_ui->version->text(), m_ui->summary->toPlainText(), m_ui->optionalFiles->isChecked(), task = new ModrinthPackExportTask(name, m_ui->version->text(), m_ui->summary->toPlainText(), m_ui->optionalFiles->isChecked(),
m_instance, output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1)); m_instance, output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1));
} else { } else {
task = new FlamePackExportTask(name, m_ui->version->text(), m_ui->author->text(), m_ui->optionalFiles->isChecked(), m_instance, FlamePackExportOptions options{};
output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1));
options.name = name;
options.version = m_ui->version->text();
options.author = m_ui->author->text();
options.optionalFiles = m_ui->optionalFiles->isChecked();
options.instance = m_instance;
options.output = output;
options.filter = std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1);
options.recommendedRAM = m_ui->recommendedMemoryCheckBox->isChecked() ? m_ui->recommendedMemory->value() : 0;
task = new FlamePackExportTask(std::move(options));
} }
connect(task, &Task::failed, connect(task, &Task::failed,

View file

@ -22,6 +22,7 @@
#include "BaseInstance.h" #include "BaseInstance.h"
#include "FastFileIconProvider.h" #include "FastFileIconProvider.h"
#include "FileIgnoreProxy.h" #include "FileIgnoreProxy.h"
#include "minecraft/MinecraftInstance.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
namespace Ui { namespace Ui {
@ -32,7 +33,7 @@ class ExportPackDialog : public QDialog {
Q_OBJECT Q_OBJECT
public: public:
explicit ExportPackDialog(InstancePtr instance, explicit ExportPackDialog(MinecraftInstancePtr instance,
QWidget* parent = nullptr, QWidget* parent = nullptr,
ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH); ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH);
~ExportPackDialog(); ~ExportPackDialog();
@ -44,7 +45,7 @@ class ExportPackDialog : public QDialog {
QString ignoreFileName(); QString ignoreFileName();
private: private:
const InstancePtr m_instance; const MinecraftInstancePtr m_instance;
Ui::ExportPackDialog* m_ui; Ui::ExportPackDialog* m_ui;
FileIgnoreProxy* m_proxy; FileIgnoreProxy* m_proxy;
FastFileIconProvider m_icons; FastFileIconProvider m_icons;

View file

@ -19,36 +19,56 @@
<property name="title"> <property name="title">
<string>&amp;Description</string> <string>&amp;Description</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_3"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QLabel" name="nameLabel"> <layout class="QFormLayout" name="formLayout">
<property name="text"> <property name="labelAlignment">
<string>&amp;Name</string> <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property> </property>
<property name="buddy"> <item row="0" column="0">
<cstring>name</cstring> <widget class="QLabel" name="nameLabel">
</property> <property name="text">
</widget> <string>&amp;Name:</string>
</item> </property>
<item> <property name="buddy">
<widget class="QLineEdit" name="name"/> <cstring>name</cstring>
</item> </property>
<item> </widget>
<widget class="QLabel" name="versionLabel"> </item>
<property name="text"> <item row="0" column="1">
<string>&amp;Version</string> <widget class="QLineEdit" name="name"/>
</property> </item>
<property name="buddy"> <item row="1" column="0">
<cstring>version</cstring> <widget class="QLabel" name="versionLabel">
</property> <property name="text">
</widget> <string>&amp;Version:</string>
</item> </property>
<item> <property name="buddy">
<widget class="QLineEdit" name="version"> <cstring>version</cstring>
<property name="text"> </property>
<string>1.0.0</string> </widget>
</property> </item>
</widget> <item row="1" column="1">
<widget class="QLineEdit" name="version">
<property name="text">
<string>1.0.0</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="authorLabel">
<property name="text">
<string>&amp;Author:</string>
</property>
<property name="buddy">
<cstring>author</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="author"/>
</item>
</layout>
</item> </item>
<item> <item>
<widget class="QLabel" name="summaryLabel"> <widget class="QLabel" name="summaryLabel">
@ -62,24 +82,29 @@
</item> </item>
<item> <item>
<widget class="QPlainTextEdit" name="summary"> <widget class="QPlainTextEdit" name="summary">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>100</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>100</height>
</size>
</property>
<property name="tabChangesFocus"> <property name="tabChangesFocus">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QLabel" name="authorLabel">
<property name="text">
<string>&amp;Author</string>
</property>
<property name="buddy">
<cstring>author</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="author"/>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -88,7 +113,70 @@
<property name="title"> <property name="title">
<string>&amp;Options</string> <string>&amp;Options</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QWidget" name="recommendedMemoryWidget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="recommendedMemoryCheckBox">
<property name="text">
<string>&amp;Recommended Memory:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="recommendedMemory">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="suffix">
<string> MiB</string>
</property>
<property name="minimum">
<number>8</number>
</property>
<property name="maximum">
<number>32768</number>
</property>
<property name="singleStep">
<number>128</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item> <item>
<widget class="QLabel" name="filesLabel"> <widget class="QLabel" name="filesLabel">
<property name="text"> <property name="text">
@ -138,10 +226,6 @@
</layout> </layout>
</widget> </widget>
<tabstops> <tabstops>
<tabstop>name</tabstop>
<tabstop>version</tabstop>
<tabstop>summary</tabstop>
<tabstop>author</tabstop>
<tabstop>files</tabstop> <tabstop>files</tabstop>
<tabstop>optionalFiles</tabstop> <tabstop>optionalFiles</tabstop>
</tabstops> </tabstops>

View file

@ -36,7 +36,6 @@
#include "MSALoginDialog.h" #include "MSALoginDialog.h"
#include "Application.h" #include "Application.h"
#include "qr.h"
#include "ui_MSALoginDialog.h" #include "ui_MSALoginDialog.h"
#include "DesktopServices.h" #include "DesktopServices.h"
@ -44,10 +43,15 @@
#include <QApplication> #include <QApplication>
#include <QClipboard> #include <QClipboard>
#include <QColor>
#include <QPainter>
#include <QPixmap> #include <QPixmap>
#include <QSize>
#include <QUrl> #include <QUrl>
#include <QtWidgets/QPushButton> #include <QtWidgets/QPushButton>
#include "qrcodegen.hpp"
MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog) MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog)
{ {
ui->setupUi(this); ui->setupUi(this);
@ -139,6 +143,33 @@ void MSALoginDialog::authorizeWithBrowser(const QUrl& url)
m_url = url; m_url = url;
} }
// https://stackoverflow.com/questions/21400254/how-to-draw-a-qr-code-with-qt-in-native-c-c
void paintQR(QPainter& painter, const QSize sz, const QString& data, QColor fg)
{
// NOTE: At this point you will use the API to get the encoding and format you want, instead of my hardcoded stuff:
qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data.toUtf8().constData(), qrcodegen::QrCode::Ecc::LOW);
const int s = qr.getSize() > 0 ? qr.getSize() : 1;
const double w = sz.width();
const double h = sz.height();
const double aspect = w / h;
const double size = ((aspect > 1.0) ? h : w);
const double scale = size / (s + 2);
// NOTE: For performance reasons my implementation only draws the foreground parts in supplied color.
// It expects background to be prepared already (in white or whatever is preferred).
painter.setPen(Qt::NoPen);
painter.setBrush(fg);
for (int y = 0; y < s; y++) {
for (int x = 0; x < s; x++) {
const int color = qr.getModule(x, y); // 0 for white, 1 for black
if (0 != color) {
const double rx1 = (x + 1) * scale, ry1 = (y + 1) * scale;
QRectF r(rx1, ry1, scale, scale);
painter.drawRects(&r, 1);
}
}
}
}
void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, [[maybe_unused]] int expiresIn) void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, [[maybe_unused]] int expiresIn)
{ {
ui->stackedWidget->setCurrentIndex(1); ui->stackedWidget->setCurrentIndex(1);

View file

@ -92,6 +92,10 @@ SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct)
connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
SLOT(selectionChanged(QItemSelection, QItemSelection))); SLOT(selectionChanged(QItemSelection, QItemSelection)));
connect(m_ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu); connect(m_ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu);
connect(m_ui->elytraCB, &QCheckBox::stateChanged, this, [this]() {
m_skinPreview->setElytraVisible(m_ui->elytraCB->isChecked());
on_capeCombo_currentIndexChanged(0);
});
setupCapes(); setupCapes();
@ -159,10 +163,24 @@ void SkinManageDialog::on_fileBtn_clicked()
} }
} }
QPixmap previewCape(QImage capeImage) QPixmap previewCape(QImage capeImage, bool elytra = false)
{ {
if (elytra) {
auto wing = capeImage.copy(34, 0, 12, 22);
QImage mirrored = wing.mirrored(true, false);
QImage combined(wing.width() * 2 - 2, wing.height(), capeImage.format());
combined.fill(Qt::transparent);
QPainter painter(&combined);
painter.drawImage(0, 0, wing);
painter.drawImage(wing.width() - 2, 0, mirrored);
painter.end();
return QPixmap::fromImage(combined.scaled(96, 176, Qt::IgnoreAspectRatio, Qt::FastTransformation));
}
return QPixmap::fromImage(capeImage.copy(1, 1, 10, 16).scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation)); return QPixmap::fromImage(capeImage.copy(1, 1, 10, 16).scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation));
} }
void SkinManageDialog::setupCapes() void SkinManageDialog::setupCapes()
{ {
// FIXME: add a model for this, download/refresh the capes on demand // FIXME: add a model for this, download/refresh the capes on demand
@ -208,7 +226,7 @@ void SkinManageDialog::setupCapes()
} }
} }
if (!capeImage.isNull()) { if (!capeImage.isNull()) {
m_ui->capeCombo->addItem(previewCape(capeImage), cape.alias, cape.id); m_ui->capeCombo->addItem(previewCape(capeImage, m_ui->elytraCB->isChecked()), cape.alias, cape.id);
} else { } else {
m_ui->capeCombo->addItem(cape.alias, cape.id); m_ui->capeCombo->addItem(cape.alias, cape.id);
} }
@ -222,7 +240,8 @@ void SkinManageDialog::on_capeCombo_currentIndexChanged(int index)
auto id = m_ui->capeCombo->currentData(); auto id = m_ui->capeCombo->currentData();
auto cape = m_capes.value(id.toString(), {}); auto cape = m_capes.value(id.toString(), {});
if (!cape.isNull()) { if (!cape.isNull()) {
m_ui->capeImage->setPixmap(previewCape(cape).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); m_ui->capeImage->setPixmap(
previewCape(cape, m_ui->elytraCB->isChecked()).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation));
} else { } else {
m_ui->capeImage->clear(); m_ui->capeImage->clear();
} }
@ -319,14 +338,14 @@ bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev)
return QDialog::eventFilter(obj, ev); return QDialog::eventFilter(obj, ev);
} }
void SkinManageDialog::on_action_Rename_Skin_triggered(bool checked) void SkinManageDialog::on_action_Rename_Skin_triggered(bool)
{ {
if (!m_selectedSkinKey.isEmpty()) { if (!m_selectedSkinKey.isEmpty()) {
m_ui->listView->edit(m_ui->listView->currentIndex()); m_ui->listView->edit(m_ui->listView->currentIndex());
} }
} }
void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked) void SkinManageDialog::on_action_Delete_Skin_triggered(bool)
{ {
if (m_selectedSkinKey.isEmpty()) if (m_selectedSkinKey.isEmpty())
return; return;
@ -523,7 +542,7 @@ void SkinManageDialog::resizeEvent(QResizeEvent* event)
auto id = m_ui->capeCombo->currentData(); auto id = m_ui->capeCombo->currentData();
auto cape = m_capes.value(id.toString(), {}); auto cape = m_capes.value(id.toString(), {});
if (!cape.isNull()) { if (!cape.isNull()) {
m_ui->capeImage->setPixmap(previewCape(cape).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); m_ui->capeImage->setPixmap(previewCape(cape, m_ui->elytraCB->isChecked()).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation));
} else { } else {
m_ui->capeImage->clear(); m_ui->capeImage->clear();
} }

View file

@ -59,6 +59,13 @@
<string>Cape</string> <string>Cape</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_4"> <layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QCheckBox" name="elytraCB">
<property name="text">
<string>Preview Elytra</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QComboBox" name="capeCombo"/> <widget class="QComboBox" name="capeCombo"/>
</item> </item>

View file

@ -180,7 +180,8 @@ QList<QVector2D> getCubeUVs(float u, float v, float width, float height, float d
} }
namespace opengl { namespace opengl {
BoxGeometry::BoxGeometry(QVector3D size, QVector3D position) : m_indexBuf(QOpenGLBuffer::IndexBuffer), m_size(size), m_position(position) BoxGeometry::BoxGeometry(QVector3D size, QVector3D position)
: QOpenGLFunctions(), m_indexBuf(QOpenGLBuffer::IndexBuffer), m_size(size), m_position(position)
{ {
initializeOpenGLFunctions(); initializeOpenGLFunctions();
@ -274,4 +275,9 @@ BoxGeometry* BoxGeometry::Plane()
return b; return b;
} }
void BoxGeometry::scale(const QVector3D& vector)
{
m_matrix.scale(vector);
}
} // namespace opengl } // namespace opengl

View file

@ -36,6 +36,7 @@ class BoxGeometry : protected QOpenGLFunctions {
void initGeometry(float u, float v, float width, float height, float depth, float textureWidth = 64, float textureHeight = 64); void initGeometry(float u, float v, float width, float height, float depth, float textureWidth = 64, float textureHeight = 64);
void rotate(float angle, const QVector3D& vector); void rotate(float angle, const QVector3D& vector);
void scale(const QVector3D& vector);
private: private:
QOpenGLBuffer m_vertexBuf; QOpenGLBuffer m_vertexBuf;

View file

@ -18,9 +18,16 @@
*/ */
#include "ui/dialogs/skins/draw/Scene.h" #include "ui/dialogs/skins/draw/Scene.h"
#include <QOpenGLFunctions>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
#include <QOpenGLWindow>
namespace opengl { namespace opengl {
Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : m_slim(slim), m_capeVisible(!cape.isNull()) Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : QOpenGLFunctions(), m_slim(slim), m_capeVisible(!cape.isNull())
{ {
initializeOpenGLFunctions();
m_staticComponents = { m_staticComponents = {
// head // head
new opengl::BoxGeometry(QVector3D(8, 8, 8), QVector3D(0, 4, 0), QPoint(0, 0), QVector3D(8, 8, 8)), new opengl::BoxGeometry(QVector3D(8, 8, 8), QVector3D(0, 4, 0), QPoint(0, 0), QVector3D(8, 8, 8)),
@ -57,6 +64,19 @@ Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : m_slim(slim),
m_cape->rotate(10.8, QVector3D(1, 0, 0)); m_cape->rotate(10.8, QVector3D(1, 0, 0));
m_cape->rotate(180, QVector3D(0, 1, 0)); m_cape->rotate(180, QVector3D(0, 1, 0));
auto leftWing =
new opengl::BoxGeometry(QVector3D(12, 22, 4), QVector3D(0, -13, -2), QPoint(22, 0), QVector3D(10, 20, 2), QSize(64, 32));
leftWing->rotate(15, QVector3D(1, 0, 0));
leftWing->rotate(15, QVector3D(0, 0, 1));
leftWing->rotate(1, QVector3D(1, 0, 0));
auto rightWing =
new opengl::BoxGeometry(QVector3D(12, 22, 4), QVector3D(0, -13, -2), QPoint(22, 0), QVector3D(10, 20, 2), QSize(64, 32));
rightWing->scale(QVector3D(-1, 1, 1));
rightWing->rotate(15, QVector3D(1, 0, 0));
rightWing->rotate(15, QVector3D(0, 0, 1));
rightWing->rotate(1, QVector3D(1, 0, 0));
m_elytra << leftWing << rightWing;
// texture init // texture init
m_skinTexture = new QOpenGLTexture(skin.mirrored()); m_skinTexture = new QOpenGLTexture(skin.mirrored());
m_skinTexture->setMinificationFilter(QOpenGLTexture::Nearest); m_skinTexture->setMinificationFilter(QOpenGLTexture::Nearest);
@ -68,7 +88,7 @@ Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : m_slim(slim),
} }
Scene::~Scene() Scene::~Scene()
{ {
for (auto array : { m_staticComponents, m_normalArms, m_slimArms }) { for (auto array : { m_staticComponents, m_normalArms, m_slimArms, m_elytra }) {
for (auto g : array) { for (auto g : array) {
delete g; delete g;
} }
@ -95,7 +115,15 @@ void Scene::draw(QOpenGLShaderProgram* program)
if (m_capeVisible) { if (m_capeVisible) {
m_capeTexture->bind(); m_capeTexture->bind();
program->setUniformValue("texture", 0); program->setUniformValue("texture", 0);
m_cape->draw(program); if (!m_elytraVisible) {
m_cape->draw(program);
} else {
glDisable(GL_CULL_FACE);
for (auto e : m_elytra) {
e->draw(program);
}
glEnable(GL_CULL_FACE);
}
m_capeTexture->release(); m_capeTexture->release();
} }
} }
@ -131,4 +159,8 @@ void Scene::setCapeVisible(bool visible)
{ {
m_capeVisible = visible; m_capeVisible = visible;
} }
void Scene::setElytraVisible(bool elytraVisible)
{
m_elytraVisible = elytraVisible;
}
} // namespace opengl } // namespace opengl

View file

@ -22,7 +22,7 @@
#include <QOpenGLTexture> #include <QOpenGLTexture>
namespace opengl { namespace opengl {
class Scene { class Scene : protected QOpenGLFunctions {
public: public:
Scene(const QImage& skin, bool slim, const QImage& cape); Scene(const QImage& skin, bool slim, const QImage& cape);
virtual ~Scene(); virtual ~Scene();
@ -32,15 +32,18 @@ class Scene {
void setCape(const QImage& cape); void setCape(const QImage& cape);
void setMode(bool slim); void setMode(bool slim);
void setCapeVisible(bool visible); void setCapeVisible(bool visible);
void setElytraVisible(bool elytraVisible);
private: private:
QList<BoxGeometry*> m_staticComponents; QList<BoxGeometry*> m_staticComponents;
QList<BoxGeometry*> m_normalArms; QList<BoxGeometry*> m_normalArms;
QList<BoxGeometry*> m_slimArms; QList<BoxGeometry*> m_slimArms;
BoxGeometry* m_cape = nullptr; BoxGeometry* m_cape = nullptr;
QList<BoxGeometry*> m_elytra;
QOpenGLTexture* m_skinTexture = nullptr; QOpenGLTexture* m_skinTexture = nullptr;
QOpenGLTexture* m_capeTexture = nullptr; QOpenGLTexture* m_capeTexture = nullptr;
bool m_slim = false; bool m_slim = false;
bool m_capeVisible = false; bool m_capeVisible = false;
bool m_elytraVisible = false;
}; };
} // namespace opengl } // namespace opengl

View file

@ -263,3 +263,8 @@ void SkinOpenGLWindow::wheelEvent(QWheelEvent* event)
m_distance = qMax(16.f, m_distance); // Clamp distance m_distance = qMax(16.f, m_distance); // Clamp distance
update(); // Trigger a repaint update(); // Trigger a repaint
} }
void SkinOpenGLWindow::setElytraVisible(bool visible)
{
if (m_scene)
m_scene->setElytraVisible(visible);
}

View file

@ -43,6 +43,7 @@ class SkinOpenGLWindow : public QOpenGLWindow, protected QOpenGLFunctions {
void updateScene(SkinModel* skin); void updateScene(SkinModel* skin);
void updateCape(const QImage& cape); void updateCape(const QImage& cape);
void setElytraVisible(bool visible);
protected: protected:
void mousePressEvent(QMouseEvent* e) override; void mousePressEvent(QMouseEvent* e) override;

View file

@ -62,7 +62,7 @@
JavaPage::JavaPage(QWidget* parent) : QWidget(parent), ui(new Ui::JavaPage) JavaPage::JavaPage(QWidget* parent) : QWidget(parent), ui(new Ui::JavaPage)
{ {
ui->setupUi(this); ui->setupUi(this);
if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { if (BuildConfig.JAVA_DOWNLOADER_ENABLED) {
ui->managedJavaList->initialize(new JavaInstallList(this, true)); ui->managedJavaList->initialize(new JavaInstallList(this, true));
ui->managedJavaList->setResizeOn(2); ui->managedJavaList->setResizeOn(2);

View file

@ -37,11 +37,11 @@
#include <Application.h> #include <Application.h>
#include <QObjectPtr.h> #include <QObjectPtr.h>
#include "ui/widgets/JavaSettingsWidget.h"
#include <QDialog> #include <QDialog>
#include <QStringListModel> #include <QStringListModel>
#include "JavaCommon.h" #include "JavaCommon.h"
#include "ui/pages/BasePage.h" #include "ui/pages/BasePage.h"
#include "ui/widgets/JavaSettingsWidget.h"
class SettingsObject; class SettingsObject;

View file

@ -35,17 +35,18 @@
#pragma once #pragma once
#include <QWidget>
#include "Application.h" #include "Application.h"
#include "BaseInstance.h" #include "BaseInstance.h"
#include "ui/pages/BasePage.h" #include "ui/pages/BasePage.h"
#include "ui/widgets/MinecraftSettingsWidget.h" #include "ui/widgets/MinecraftSettingsWidget.h"
#include <QWidget>
class InstanceSettingsPage : public MinecraftSettingsWidget, public BasePage { class InstanceSettingsPage : public MinecraftSettingsWidget, public BasePage {
Q_OBJECT Q_OBJECT
public: public:
explicit InstanceSettingsPage(MinecraftInstancePtr instance, QWidget* parent = nullptr) : MinecraftSettingsWidget(std::move(instance), parent) explicit InstanceSettingsPage(MinecraftInstancePtr instance, QWidget* parent = nullptr)
: MinecraftSettingsWidget(std::move(instance), parent)
{ {
connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::saveSettings); connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::saveSettings);
connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings);

View file

@ -347,13 +347,18 @@ void ManagedPackPage::onUpdateTaskCompleted(bool did_succeed) const
if (m_instance_window != nullptr) if (m_instance_window != nullptr)
m_instance_window->close(); 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) CustomMessageBox::selectable(nullptr, tr("Update Successful"),
->show(); tr("The instance updated to pack version %1 successfully.").arg(m_inst->getManagedPackVersionName()),
QMessageBox::Information)
->show();
} else { } 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) CustomMessageBox::selectable(
->show(); 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() void ModrinthManagedPackPage::update()

View file

@ -1,21 +1,22 @@
#include <QObject> #include <qtconcurrentrun.h>
#include <QTcpSocket>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <qtconcurrentrun.h> #include <QObject>
#include <QTcpSocket>
#include <Exception.h> #include <Exception.h>
#include "McClient.h"
#include "Json.h" #include "Json.h"
#include "McClient.h"
// 7 first bits // 7 first bits
#define SEGMENT_BITS 0x7F #define SEGMENT_BITS 0x7F
// last bit // last bit
#define CONTINUE_BIT 0x80 #define CONTINUE_BIT 0x80
McClient::McClient(QObject *parent, QString domain, QString ip, short port): QObject(parent), m_domain(domain), m_ip(ip), m_port(port) {} McClient::McClient(QObject* parent, QString domain, QString ip, short port) : QObject(parent), m_domain(domain), m_ip(ip), m_port(port) {}
void McClient::getStatusData() { void McClient::getStatusData()
{
qDebug() << "Connecting to socket.."; qDebug() << "Connecting to socket..";
connect(&m_socket, &QTcpSocket::connected, this, [this]() { connect(&m_socket, &QTcpSocket::connected, this, [this]() {
@ -25,28 +26,28 @@ void McClient::getStatusData() {
connect(&m_socket, &QTcpSocket::readyRead, this, &McClient::readRawResponse); connect(&m_socket, &QTcpSocket::readyRead, this, &McClient::readRawResponse);
}); });
connect(&m_socket, &QTcpSocket::errorOccurred, this, [this]() { connect(&m_socket, &QTcpSocket::errorOccurred, this, [this]() { emitFail("Socket disconnected: " + m_socket.errorString()); });
emitFail("Socket disconnected: " + m_socket.errorString());
});
m_socket.connectToHost(m_ip, m_port); m_socket.connectToHost(m_ip, m_port);
} }
void McClient::sendRequest() { void McClient::sendRequest()
{
QByteArray data; QByteArray data;
writeVarInt(data, 0x00); // packet ID writeVarInt(data, 0x00); // packet ID
writeVarInt(data, 763); // hardcoded protocol version (763 = 1.20.1) writeVarInt(data, 763); // hardcoded protocol version (763 = 1.20.1)
writeVarInt(data, m_domain.size()); // server address length writeVarInt(data, m_domain.size()); // server address length
writeString(data, m_domain.toStdString()); // server address writeString(data, m_domain.toStdString()); // server address
writeFixedInt(data, m_port, 2); // server port writeFixedInt(data, m_port, 2); // server port
writeVarInt(data, 0x01); // next state writeVarInt(data, 0x01); // next state
writePacketToSocket(data); // send handshake packet writePacketToSocket(data); // send handshake packet
writeVarInt(data, 0x00); // packet ID writeVarInt(data, 0x00); // packet ID
writePacketToSocket(data); // send status packet writePacketToSocket(data); // send status packet
} }
void McClient::readRawResponse() { void McClient::readRawResponse()
{
if (m_responseReadState == 2) { if (m_responseReadState == 2) {
return; return;
} }
@ -56,28 +57,27 @@ void McClient::readRawResponse() {
m_wantedRespLength = readVarInt(m_resp); m_wantedRespLength = readVarInt(m_resp);
m_responseReadState = 1; m_responseReadState = 1;
} }
if (m_responseReadState == 1 && m_resp.size() >= m_wantedRespLength) { if (m_responseReadState == 1 && m_resp.size() >= m_wantedRespLength) {
if (m_resp.size() > m_wantedRespLength) { if (m_resp.size() > m_wantedRespLength) {
qDebug() << "Warning: Packet length doesn't match actual packet size (" << m_wantedRespLength << " expected vs " << m_resp.size() << " received)"; qDebug() << "Warning: Packet length doesn't match actual packet size (" << m_wantedRespLength << " expected vs "
<< m_resp.size() << " received)";
} }
parseResponse(); parseResponse();
m_responseReadState = 2; m_responseReadState = 2;
} }
} }
void McClient::parseResponse() { void McClient::parseResponse()
{
qDebug() << "Received response successfully"; qDebug() << "Received response successfully";
int packetID = readVarInt(m_resp); int packetID = readVarInt(m_resp);
if (packetID != 0x00) { if (packetID != 0x00) {
throw Exception( throw Exception(QString("Packet ID doesn't match expected value (0x00 vs 0x%1)").arg(packetID, 0, 16));
QString("Packet ID doesn't match expected value (0x00 vs 0x%1)")
.arg(packetID, 0, 16)
);
} }
Q_UNUSED(readVarInt(m_resp)); // json length Q_UNUSED(readVarInt(m_resp)); // json length
// 'resp' should now be the JSON string // 'resp' should now be the JSON string
QJsonDocument doc = QJsonDocument::fromJson(m_resp); QJsonDocument doc = QJsonDocument::fromJson(m_resp);
@ -85,8 +85,9 @@ void McClient::parseResponse() {
} }
// From https://wiki.vg/Protocol#VarInt_and_VarLong // From https://wiki.vg/Protocol#VarInt_and_VarLong
void McClient::writeVarInt(QByteArray &data, int value) { void McClient::writeVarInt(QByteArray& data, int value)
while ((value & ~SEGMENT_BITS)) { // check if the value is too big to fit in 7 bits {
while ((value & ~SEGMENT_BITS)) { // check if the value is too big to fit in 7 bits
// Write 7 bits // Write 7 bits
data.append((value & SEGMENT_BITS) | CONTINUE_BIT); data.append((value & SEGMENT_BITS) | CONTINUE_BIT);
@ -98,7 +99,8 @@ void McClient::writeVarInt(QByteArray &data, int value) {
} }
// From https://wiki.vg/Protocol#VarInt_and_VarLong // From https://wiki.vg/Protocol#VarInt_and_VarLong
int McClient::readVarInt(QByteArray &data) { int McClient::readVarInt(QByteArray& data)
{
int value = 0; int value = 0;
int position = 0; int position = 0;
char currentByte; char currentByte;
@ -107,17 +109,20 @@ int McClient::readVarInt(QByteArray &data) {
currentByte = readByte(data); currentByte = readByte(data);
value |= (currentByte & SEGMENT_BITS) << position; value |= (currentByte & SEGMENT_BITS) << position;
if ((currentByte & CONTINUE_BIT) == 0) break; if ((currentByte & CONTINUE_BIT) == 0)
break;
position += 7; position += 7;
} }
if (position >= 32) throw Exception("VarInt is too big"); if (position >= 32)
throw Exception("VarInt is too big");
return value; return value;
} }
char McClient::readByte(QByteArray &data) { char McClient::readByte(QByteArray& data)
{
if (data.isEmpty()) { if (data.isEmpty()) {
throw Exception("No more bytes to read"); throw Exception("No more bytes to read");
} }
@ -128,17 +133,20 @@ char McClient::readByte(QByteArray &data) {
} }
// write number with specified size in big endian format // write number with specified size in big endian format
void McClient::writeFixedInt(QByteArray &data, int value, int size) { void McClient::writeFixedInt(QByteArray& data, int value, int size)
{
for (int i = size - 1; i >= 0; i--) { for (int i = size - 1; i >= 0; i--) {
data.append((value >> (i * 8)) & 0xFF); data.append((value >> (i * 8)) & 0xFF);
} }
} }
void McClient::writeString(QByteArray &data, const std::string &value) { void McClient::writeString(QByteArray& data, const std::string& value)
{
data.append(value.c_str()); data.append(value.c_str());
} }
void McClient::writePacketToSocket(QByteArray &data) { void McClient::writePacketToSocket(QByteArray& data)
{
// we prefix the packet with its length // we prefix the packet with its length
QByteArray dataWithSize; QByteArray dataWithSize;
writeVarInt(dataWithSize, data.size()); writeVarInt(dataWithSize, data.size());
@ -151,14 +159,15 @@ void McClient::writePacketToSocket(QByteArray &data) {
data.clear(); data.clear();
} }
void McClient::emitFail(QString error)
void McClient::emitFail(QString error) { {
qDebug() << "Minecraft server ping for status error:" << error; qDebug() << "Minecraft server ping for status error:" << error;
emit failed(error); emit failed(error);
emit finished(); emit finished();
} }
void McClient::emitSucceed(QJsonObject data) { void McClient::emitSucceed(QJsonObject data)
{
emit succeeded(data); emit succeeded(data);
emit finished(); emit finished();
} }

View file

@ -1,8 +1,8 @@
#include <QObject> #include <QFuture>
#include <QTcpSocket>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QFuture> #include <QObject>
#include <QTcpSocket>
#include <Exception.h> #include <Exception.h>
@ -22,29 +22,30 @@ class McClient : public QObject {
unsigned m_wantedRespLength = 0; unsigned m_wantedRespLength = 0;
QByteArray m_resp; QByteArray m_resp;
public: public:
explicit McClient(QObject *parent, QString domain, QString ip, short port); explicit McClient(QObject* parent, QString domain, QString ip, short port);
//! Read status data of the server, and calls the succeeded() signal with the parsed JSON data //! Read status data of the server, and calls the succeeded() signal with the parsed JSON data
void getStatusData(); void getStatusData();
private:
private:
void sendRequest(); void sendRequest();
//! Accumulate data until we have a full response, then call parseResponse() once //! Accumulate data until we have a full response, then call parseResponse() once
void readRawResponse(); void readRawResponse();
void parseResponse(); void parseResponse();
void writeVarInt(QByteArray &data, int value); void writeVarInt(QByteArray& data, int value);
int readVarInt(QByteArray &data); int readVarInt(QByteArray& data);
char readByte(QByteArray &data); char readByte(QByteArray& data);
//! write number with specified size in big endian format //! write number with specified size in big endian format
void writeFixedInt(QByteArray &data, int value, int size); void writeFixedInt(QByteArray& data, int value, int size);
void writeString(QByteArray &data, const std::string &value); void writeString(QByteArray& data, const std::string& value);
void writePacketToSocket(QByteArray &data); void writePacketToSocket(QByteArray& data);
void emitFail(QString error); void emitFail(QString error);
void emitSucceed(QJsonObject data); void emitSucceed(QJsonObject data);
signals: signals:
void succeeded(QJsonObject data); void succeeded(QJsonObject data);
void failed(QString error); void failed(QString error);
void finished(); void finished();

View file

@ -1,23 +1,25 @@
#include <QObject>
#include <QDnsLookup>
#include <QtNetwork/qtcpsocket.h> #include <QtNetwork/qtcpsocket.h>
#include <QDnsLookup>
#include <QHostInfo> #include <QHostInfo>
#include <QObject>
#include "McResolver.h" #include "McResolver.h"
McResolver::McResolver(QObject *parent, QString domain, int port): QObject(parent), m_constrDomain(domain), m_constrPort(port) {} McResolver::McResolver(QObject* parent, QString domain, int port) : QObject(parent), m_constrDomain(domain), m_constrPort(port) {}
void McResolver::ping() { void McResolver::ping()
{
pingWithDomainSRV(m_constrDomain, m_constrPort); pingWithDomainSRV(m_constrDomain, m_constrPort);
} }
void McResolver::pingWithDomainSRV(QString domain, int port) { void McResolver::pingWithDomainSRV(QString domain, int port)
QDnsLookup *lookup = new QDnsLookup(this); {
QDnsLookup* lookup = new QDnsLookup(this);
lookup->setName(QString("_minecraft._tcp.%1").arg(domain)); lookup->setName(QString("_minecraft._tcp.%1").arg(domain));
lookup->setType(QDnsLookup::SRV); lookup->setType(QDnsLookup::SRV);
connect(lookup, &QDnsLookup::finished, this, [this, domain, port]() { connect(lookup, &QDnsLookup::finished, this, [this, domain, port]() {
QDnsLookup *lookup = qobject_cast<QDnsLookup *>(sender()); QDnsLookup* lookup = qobject_cast<QDnsLookup*>(sender());
lookup->deleteLater(); lookup->deleteLater();
@ -43,8 +45,9 @@ void McResolver::pingWithDomainSRV(QString domain, int port) {
lookup->lookup(); lookup->lookup();
} }
void McResolver::pingWithDomainA(QString domain, int port) { void McResolver::pingWithDomainA(QString domain, int port)
QHostInfo::lookupHost(domain, this, [this, port](const QHostInfo &hostInfo){ {
QHostInfo::lookupHost(domain, this, [this, port](const QHostInfo& hostInfo) {
if (hostInfo.error() != QHostInfo::NoError) { if (hostInfo.error() != QHostInfo::NoError) {
emitFail("A record lookup failed"); emitFail("A record lookup failed");
return; return;
@ -55,19 +58,21 @@ void McResolver::pingWithDomainA(QString domain, int port) {
emitFail("No A entries found for domain"); emitFail("No A entries found for domain");
return; return;
} }
const auto& firstRecord = records.at(0); const auto& firstRecord = records.at(0);
emitSucceed(firstRecord.toString(), port); emitSucceed(firstRecord.toString(), port);
}); });
} }
void McResolver::emitFail(QString error) { void McResolver::emitFail(QString error)
{
qDebug() << "DNS resolver error:" << error; qDebug() << "DNS resolver error:" << error;
emit failed(error); emit failed(error);
emit finished(); emit finished();
} }
void McResolver::emitSucceed(QString ip, int port) { void McResolver::emitSucceed(QString ip, int port)
{
emit succeeded(ip, port); emit succeeded(ip, port);
emit finished(); emit finished();
} }

View file

@ -1,8 +1,8 @@
#include <QtNetwork/qtcpsocket.h>
#include <QDnsLookup>
#include <QHostInfo>
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include <QDnsLookup>
#include <QtNetwork/qtcpsocket.h>
#include <QHostInfo>
// resolve the IP and port of a Minecraft server // resolve the IP and port of a Minecraft server
class McResolver : public QObject { class McResolver : public QObject {
@ -11,17 +11,17 @@ class McResolver : public QObject {
QString m_constrDomain; QString m_constrDomain;
int m_constrPort; int m_constrPort;
public: public:
explicit McResolver(QObject *parent, QString domain, int port); explicit McResolver(QObject* parent, QString domain, int port);
void ping(); void ping();
private: private:
void pingWithDomainSRV(QString domain, int port); void pingWithDomainSRV(QString domain, int port);
void pingWithDomainA(QString domain, int port); void pingWithDomainA(QString domain, int port);
void emitFail(QString error); void emitFail(QString error);
void emitSucceed(QString ip, int port); void emitSucceed(QString ip, int port);
signals: signals:
void succeeded(QString ip, int port); void succeeded(QString ip, int port);
void failed(QString error); void failed(QString error);
void finished(); void finished();

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