Merge branch 'develop' into feature/java-downloader

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad 2023-07-16 12:32:31 +01:00 committed by GitHub
commit 5f37e339fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
879 changed files with 49762 additions and 21479 deletions

8
.editorconfig Normal file
View File

@ -0,0 +1,8 @@
# EditorConfig specs and documentation: https://EditorConfig.org
# top-most EditorConfig file
root = true
# C++ Code Style settings
[*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}]
cpp_generate_documentation_comments = doxygen_slash_star

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

4
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,4 @@
# .git-blame-ignore-revs
# tabs -> spaces
bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9

View File

@ -7,10 +7,23 @@ on:
description: Type of build (Debug, Release, RelWithDebInfo, MinSizeRel)
type: string
default: Debug
is_qt_cached:
description: Enable Qt caching or not
type: string
default: true
secrets:
SPARKLE_ED25519_KEY:
description: Private key for signing Sparkle updates
required: false
WINDOWS_CODESIGN_CERT:
description: Certificate for signing Windows builds
required: false
WINDOWS_CODESIGN_PASSWORD:
description: Password for signing Windows builds
required: false
CACHIX_AUTH_TOKEN:
description: Private token for authenticating against Cachix cache
required: false
jobs:
build:
@ -25,26 +38,61 @@ jobs:
- os: ubuntu-20.04
qt_ver: 6
qt_host: linux
qt_arch: ''
qt_version: '6.2.4'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
- os: windows-2022
name: "Windows-Legacy"
msystem: mingw32
name: "Windows-MinGW-w64"
msystem: clang64
vcvars_arch: 'amd64_x86'
- os: windows-2022
name: "Windows-MSVC-Legacy"
msystem: ''
architecture: 'win32'
vcvars_arch: 'amd64_x86'
qt_ver: 5
qt_host: windows
qt_arch: 'win32_msvc2019'
qt_version: '5.15.2'
qt_modules: ''
qt_tools: 'tools_openssl_x86'
- os: windows-2022
name: "Windows"
msystem: mingw32
name: "Windows-MSVC"
msystem: ''
architecture: 'x64'
vcvars_arch: 'amd64'
qt_ver: 6
qt_host: windows
qt_arch: ''
qt_version: '6.5.1'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
- os: windows-2022
name: "Windows-MSVC-arm64"
msystem: ''
architecture: 'arm64'
vcvars_arch: 'amd64_arm64'
qt_ver: 6
qt_host: windows
qt_arch: 'win64_msvc2019_arm64'
qt_version: '6.5.1'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
- os: macos-12
name: macOS
macosx_deployment_target: 10.15
macosx_deployment_target: 11.0
qt_ver: 6
qt_host: mac
qt_version: '6.3.0'
qt_arch: ''
qt_version: '6.5.0'
qt_modules: 'qt5compat qtimageformats'
qt_tools: ''
- os: macos-12
name: macOS-Legacy
@ -53,6 +101,7 @@ jobs:
qt_host: mac
qt_version: '5.15.2'
qt_modules: ''
qt_tools: ''
runs-on: ${{ matrix.os }}
@ -63,6 +112,7 @@ jobs:
INSTALL_APPIMAGE_DIR: "install-appdir"
BUILD_DIR: "build"
CCACHE_VAR: ""
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
steps:
##
@ -73,43 +123,50 @@ jobs:
with:
submodules: 'true'
- name: Initialize CodeQL
if: runner.os == 'Linux' && matrix.qt_ver == 6
uses: github/codeql-action/init@v2
with:
config-file: ./.github/codeql/codeql-config.yml
queries: security-and-quality
languages: cpp, java
- name: 'Setup MSYS2'
if: runner.os == 'Windows'
if: runner.os == 'Windows' && matrix.msystem != ''
uses: msys2/setup-msys2@v2
with:
msystem: ${{ matrix.msystem }}
update: true
install: >-
git
mingw-w64-x86_64-binutils
pacboy: >-
toolchain:p
cmake:p
extra-cmake-modules:p
ninja:p
qt${{ matrix.qt_ver }}-base:p
qt${{ matrix.qt_ver }}-svg:p
qt${{ matrix.qt_ver }}-imageformats:p
quazip-qt${{ matrix.qt_ver }}:p
qt6-base:p
qt6-svg:p
qt6-imageformats:p
quazip-qt6:p
ccache:p
nsis:p
${{ matrix.qt_ver == 6 && 'qt6-5compat:p' || '' }}
qt6-5compat: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' && inputs.build_type == 'Debug'
uses: hendrikmuhs/ccache-action@v1.2.3
if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug'
uses: hendrikmuhs/ccache-action@v1.2.9
with:
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
- name: Setup ccache (Windows)
if: runner.os == 'Windows' && inputs.build_type == 'Debug'
- name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
uses: actions/cache@v3.3.1
with:
path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
restore-keys: |
${{ matrix.os }}-mingw-w64-ccache
- name: Setup ccache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
shell: msys2 {0}
run: |
ccache --set-config=cache_dir='${{ github.workspace }}\.ccache'
@ -124,15 +181,6 @@ jobs:
run: |
echo "CCACHE_VAR=ccache" >> $GITHUB_ENV
- name: Retrieve ccache cache (Windows)
if: runner.os == 'Windows' && inputs.build_type == 'Debug'
uses: actions/cache@v3.0.11
with:
path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}
restore-keys: |
${{ matrix.os }}-qt${{ matrix.qt_ver }}
- name: Set short version
shell: bash
run: |
@ -143,7 +191,7 @@ jobs:
if: runner.os == 'Linux'
run: |
sudo apt-get -y update
sudo apt-get -y install ninja-build extra-cmake-modules scdoc
sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream
- name: Install Dependencies (macOS)
if: runner.os == 'macOS'
@ -155,17 +203,44 @@ jobs:
if: runner.os == 'Linux' && matrix.qt_ver != 6
run: |
sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5
- name: Install Qt (macOS and AppImage)
if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS'
- name: Install host Qt (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
uses: jurplel/install-qt-action@v3
with:
aqtversion: '==3.1.*'
py7zrversion: '>=0.20.2'
version: ${{ matrix.qt_version }}
host: 'windows'
target: 'desktop'
arch: ''
modules: ${{ matrix.qt_modules }}
tools: ${{ matrix.qt_tools }}
cache: ${{ inputs.is_qt_cached }}
cache-key-prefix: host-qt-arm64-windows
dir: ${{ github.workspace }}\HostQt
set-env: false
- name: Install Qt (macOS, Linux, Qt 6 & Windows MSVC)
if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS' || (runner.os == 'Windows' && matrix.msystem == '')
uses: jurplel/install-qt-action@v3
with:
aqtversion: '==3.1.*'
py7zrversion: '>=0.20.2'
version: ${{ matrix.qt_version }}
host: ${{ matrix.qt_host }}
target: 'desktop'
arch: ${{ matrix.qt_arch }}
modules: ${{ matrix.qt_modules }}
cache: true
cache-key-prefix: ${{ matrix.qt_host }}-${{ matrix.qt_version }}-"${{ matrix.qt_modules }}"-qt_cache
tools: ${{ matrix.qt_tools }}
cache: ${{ inputs.is_qt_cached }}
- name: Install MSVC (Windows MSVC)
if: runner.os == 'Windows' # We want this for MinGW builds as well, as we need SignTool
uses: ilammy/msvc-dev-cmd@v1
with:
vsversion: 2022
arch: ${{ matrix.vcvars_arch }}
- name: Prepare AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5
@ -175,6 +250,12 @@ jobs:
wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage"
${{ github.workspace }}/.github/scripts/prepare_JREs.sh
sudo apt install libopengl0
- name: Add QT_HOST_PATH var (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
run: |
echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2019_64" >> $env:GITHUB_ENV
##
# CONFIGURE
@ -190,11 +271,26 @@ jobs:
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -G Ninja
- name: Configure CMake (Windows)
if: runner.os == 'Windows'
- 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_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -G Ninja
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -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 -G Ninja
- name: Configure CMake (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == ''
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON
# https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix)
if ("${{ env.CCACHE_VAR }}")
{
Copy-Item C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/ccache.exe -Destination C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/cl.exe
echo "CLToolExe=cl.exe" >> $env:GITHUB_ENV
echo "CLToolPath=C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/" >> $env:GITHUB_ENV
echo "TrackFileAccess=false" >> $env:GITHUB_ENV
}
# Needed for ccache, but also speeds up compile
echo "UseMultiToolTask=true" >> $env:GITHUB_ENV
- name: Configure CMake (Linux)
if: runner.os == 'Linux'
@ -210,12 +306,17 @@ jobs:
run: |
cmake --build ${{ env.BUILD_DIR }}
- name: Build (Windows)
if: runner.os == 'Windows'
- name: Build (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != ''
shell: msys2 {0}
run: |
cmake --build ${{ env.BUILD_DIR }}
- name: Build (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == ''
run: |
cmake --build ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }}
##
# TEST
##
@ -223,21 +324,18 @@ jobs:
- name: Test
if: runner.os != 'Windows'
run: |
ctest --test-dir build --output-on-failure
ctest -E "^example64|example$" --test-dir build --output-on-failure
- name: Test (Windows)
if: runner.os == 'Windows'
- name: Test (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != ''
shell: msys2 {0}
run: |
ctest --test-dir build --output-on-failure
ctest -E "^example64|example$" --test-dir build --output-on-failure
##
# CODE SCAN
##
- name: Perform CodeQL Analysis
if: runner.os == 'Linux' && matrix.qt_ver == 6
uses: github/codeql-action/analyze@v2
- name: Test (Windows MSVC)
if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture != 'arm64'
run: |
ctest -E "^example64|example$" --test-dir build --output-on-failure -C ${{ inputs.build_type }}
##
# PACKAGE BUILDS
@ -251,6 +349,7 @@ jobs:
cd ${{ env.INSTALL_DIR }}
chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher"
sudo codesign --sign - --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
mv "PrismLauncher.app" "Prism Launcher.app"
tar -czf ../PrismLauncher.tar.gz *
- name: Make Sparkle signature (macOS)
@ -272,35 +371,83 @@ jobs:
EOF
fi
- name: Package (Windows)
if: runner.os == 'Windows'
- 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 ${{ env.INSTALL_DIR }}
if [ "${{ matrix.qt_ver }}" == "5" ]; then
cp /mingw32/bin/libcrypto-1_1.dll /mingw32/bin/libssl-1_1.dll ./
fi
if ("${{ matrix.qt_ver }}" -eq "5")
{
Copy-Item D:/a/PrismLauncher/Qt/Tools/OpenSSL/Win_x86/bin/libcrypto-1_1.dll -Destination libcrypto-1_1.dll
Copy-Item D:/a/PrismLauncher/Qt/Tools/OpenSSL/Win_x86/bin/libssl-1_1.dll -Destination libssl-1_1.dll
}
cd ${{ github.workspace }}
- name: Package (Windows, portable)
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_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'
shell: msys2 {0}
run: |
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 (Linux)
if: runner.os == 'Linux'
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_DIR }}
for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_DIR }}/manifest.txt
cd ${{ env.INSTALL_DIR }}
tar --owner root --group root -czf ../PrismLauncher.tar.gz *
@ -310,6 +457,8 @@ jobs:
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }}
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
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 *
@ -319,7 +468,8 @@ jobs:
shell: bash
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-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage"
chmod +x linuxdeploy-*.AppImage
@ -334,7 +484,8 @@ jobs:
cp -r /home/runner/work/PrismLauncher/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}//usr/lib/
cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server"
@ -412,4 +563,56 @@ jobs:
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: ccache stats (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != ''
shell: msys2 {0}
run: |
ccache -s
flatpak:
runs-on: ubuntu-latest
container:
image: bilelmoussaoui/flatpak-github-actions:kde-5.15-22.08
options: --privileged
steps:
- name: Checkout
uses: actions/checkout@v3
if: inputs.build_type == 'Debug'
with:
submodules: 'true'
- name: Build Flatpak (Linux)
if: inputs.build_type == 'Debug'
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: "Prism Launcher.flatpak"
manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml
nix:
runs-on: ubuntu-latest
strategy:
matrix:
package:
- prismlauncher
- prismlauncher-qt5
steps:
- name: Clone repository
if: inputs.build_type == 'Debug'
uses: actions/checkout@v3
with:
submodules: 'true'
- name: Install nix
if: inputs.build_type == 'Debug'
uses: cachix/install-nix-action@v22
with:
install_url: https://nixos.org/nix/install
extra_nix_config: |
auto-optimise-store = true
experimental-features = nix-command flakes
- uses: cachix/cachix-action@v12
if: inputs.build_type == 'Debug'
with:
name: prismlauncher
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build
if: inputs.build_type == 'Debug'
run: nix build .#${{ matrix.package }} --print-build-logs

35
.github/workflows/codeql.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: "CodeQL Code Scanning"
on: [ push, pull_request, workflow_dispatch ]
jobs:
CodeQL:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
submodules: 'true'
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
config-file: ./.github/codeql/codeql-config.yml
queries: security-and-quality
languages: cpp, java
- name: Install Dependencies
run:
sudo apt-get -y update
sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5
- name: Configure and Build
run: |
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr -DLauncher_QT_VERSION_MAJOR=5 -G Ninja
cmake --build build
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@ -8,7 +8,6 @@ on:
- '**.md'
- '**/LICENSE'
- 'flake.lock'
- '**.nix'
- 'packages/**'
- '.github/ISSUE_TEMPLATE/**'
- '.markdownlint**'
@ -17,7 +16,6 @@ on:
- '**.md'
- '**/LICENSE'
- 'flake.lock'
- '**.nix'
- 'packages/**'
- '.github/ISSUE_TEMPLATE/**'
- '.markdownlint**'
@ -30,5 +28,9 @@ jobs:
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 }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}

View File

@ -12,8 +12,12 @@ jobs:
uses: ./.github/workflows/build.yml
with:
build_type: Release
is_qt_cached: false
secrets:
SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }}
WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
create_release:
needs: build_release
@ -43,15 +47,28 @@ jobs:
mv PrismLauncher-macOS-Legacy*/PrismLauncher.tar.gz PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz
mv PrismLauncher-macOS*/PrismLauncher.tar.gz PrismLauncher-macOS-${{ env.VERSION }}.tar.gz
tar -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}
tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }}
for d in PrismLauncher-Windows-*; do
for d in PrismLauncher-Windows-MSVC*; do
cd "${d}" || continue
LEGACY="$(echo -n ${d} | grep -o Legacy || true)"
ARM64="$(echo -n ${d} | grep -o arm64 || true)"
INST="$(echo -n ${d} | grep -o Setup || true)"
PORT="$(echo -n ${d} | grep -o Portable || true)"
NAME="PrismLauncher-Windows"
NAME="PrismLauncher-Windows-MSVC"
test -z "${LEGACY}" || NAME="${NAME}-Legacy"
test -z "${ARM64}" || NAME="${NAME}-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
for d in PrismLauncher-Windows-MinGW-w64*; do
cd "${d}" || continue
INST="$(echo -n ${d} | grep -o Setup || true)"
PORT="$(echo -n ${d} | grep -o Portable || true)"
NAME="PrismLauncher-Windows-MinGW-w64"
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" *
@ -72,14 +89,20 @@ jobs:
PrismLauncher-Linux-${{ env.VERSION }}.tar.gz
PrismLauncher-Linux-Portable-${{ env.VERSION }}.tar.gz
PrismLauncher-Linux-${{ env.VERSION }}-x86_64.AppImage
PrismLauncher-Windows-Legacy-${{ env.VERSION }}.zip
PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz
PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz
PrismLauncher-Windows-Legacy-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-Legacy-Setup-${{ env.VERSION }}.exe
PrismLauncher-Windows-${{ env.VERSION }}.zip
PrismLauncher-Windows-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-Setup-${{ env.VERSION }}.exe
PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip
PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MinGW-w64-Setup-${{ env.VERSION }}.exe
PrismLauncher-Windows-MSVC-Legacy-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Legacy-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Legacy-Setup-${{ env.VERSION }}.exe
PrismLauncher-Windows-MSVC-arm64-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-arm64-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-arm64-Setup-${{ env.VERSION }}.exe
PrismLauncher-Windows-MSVC-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip
PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe
PrismLauncher-macOS-${{ env.VERSION }}.tar.gz
PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz
PrismLauncher-${{ env.VERSION }}.tar.gz

28
.github/workflows/update-flake.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Update Flake Lockfile
on:
schedule:
# run weekly on sunday
- cron: "0 0 * * 0"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
update-flake:
if: github.repository == 'PrismLauncher/PrismLauncher'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v22
- uses: DeterminateSystems/update-flake-lock@v19
with:
commit-msg: "chore(nix): update lockfile"
pr-title: "chore(nix): update lockfile"
pr-labels: |
Linux
simple change

View File

@ -7,9 +7,9 @@ jobs:
publish:
runs-on: windows-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@v1
- uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: PrismLauncher.PrismLauncher
version: ${{ github.event.release.tag_name }}
installers-regex: 'PrismLauncher-Windows-Setup-.+\.exe$'
installers-regex: 'PrismLauncher-Windows-MSVC(:?-arm64|-Legacy)?-Setup-.+\.exe$'
token: ${{ secrets.WINGET_TOKEN }}

11
.gitignore vendored
View File

@ -11,10 +11,14 @@ html/
*.pro.user
CMakeLists.txt.user
CMakeLists.txt.user.*
CMakeSettings.json
/CMakeFiles
CMakeCache.txt
/.project
/.settings
/.idea
/.vscode
/.vs
cmake-build-*/
Debug
@ -42,8 +46,13 @@ run/
.cache/
# Nix/NixOS
result/
.direnv/
.pre-commit-config.yaml
result
# Flatpak
.flatpak-builder
flatbuild
# Snap
*.snap

9
.gitmodules vendored
View File

@ -10,3 +10,12 @@
[submodule "libraries/libnbtplusplus"]
path = libraries/libnbtplusplus
url = https://github.com/PrismLauncher/libnbtplusplus.git
[submodule "libraries/zlib"]
path = libraries/zlib
url = https://github.com/madler/zlib.git
[submodule "libraries/extra-cmake-modules"]
path = libraries/extra-cmake-modules
url = https://github.com/KDE/extra-cmake-modules
[submodule "libraries/cmark"]
path = libraries/cmark
url = https://github.com/commonmark/cmark.git

View File

@ -1,53 +1,3 @@
# Build Instructions
Full build instructions will be available on [the website](https://prismlauncher.org/wiki/development/build-instructions/).
If you would like to contribute or fix an issue with the Build instructions you will be able to do so [here](https://github.com/PrismLauncher/website/blob/master/src/wiki/development/build-instructions.md).
## Getting the source
Clone the source code using git, and grab all the submodules. This is generic for all platforms you want to build on.
```
git clone --recursive https://github.com/PrismLauncher/PrismLauncher
cd PrismLauncher
```
## Linux
This guide will mostly mention dependant packages by their Debian naming and commands are done by a user in the sudoers file.
### Dependencies
- A C++ compiler capable of building C++17 code (can be found in the package `build-essential`).
- Qt Development tools 5.12 or newer (on Debian 11 or Debian-based distributions, `qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5`).
- `cmake` 3.15 or newer.
- `extra-cmake-modules`.
- zlib (`zlib1g-dev` on Debian 11 or Debian-based distributions).
- Java Development Kit (Java JDK) (`openjdk-17-jdk` on Debian 11 or Debian-based distributions).
- Mesa GL headers (`libgl1-mesa-dev` on Debian 11 or Debian-based distributions).
- (Optional) `scdoc` to generate man pages.
In conclusion, to check if all you need is installed (including optional):
```
sudo apt install build-essential qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 cmake extra-cmake-modules zlib1g-dev openjdk-17-jdk libgl1-mesa-dev scdoc
```
### Compiling
#### Building and installing on the system
This is usually the suggested way to build the client.
```
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="/usr" -DENABLE_LTO=ON
cmake --build build -j$(nproc)
sudo cmake --install build
```
#### Building a portable binary
```
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=install
cmake --build build -j$(nproc)
cmake --install build
cmake --install build --component portable
```
Full build instructions are available on [the website](https://prismlauncher.org/wiki/development/build-instructions/).

View File

@ -1,10 +1,5 @@
cmake_minimum_required(VERSION 3.15) # minimum version required by QuaZip
if(WIN32)
# In Qt 5.1+ we have our own main() function, don't autolink to qtmain on Windows
cmake_policy(SET CMP0020 OLD)
endif()
project(Launcher)
string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD)
@ -32,27 +27,114 @@ set(CMAKE_C_STANDARD_REQUIRED true)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_C_STANDARD 11)
include(GenerateExportHeader)
set(CMAKE_CXX_FLAGS "-Wall -pedantic -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}")
if(MSVC)
# /GS Adds buffer security checks, default on but incuded anyway to mirror gcc's fstack-protector flag
# /permissive- specify standards-conforming compiler behavior, also enabled by Qt6, default on with std:c++20
# Use /W4 as /Wall includes unnesserey warnings such as added padding to structs
set(CMAKE_CXX_FLAGS "/GS /permissive- /W4 ${CMAKE_CXX_FLAGS}")
# LINK accepts /SUBSYSTEM whics sets if we are a WINDOWS (gui) or a CONSOLE programs
# This implicitly selects an entrypoint specific to the subsystem selected
# qtmain/QtEntryPointLib provides the correct entrypoint (wWinMain) for gui programs
# Additinaly LINK autodetects we use a GUI so we can omit /SUBSYSTEM
# This allows tests to still use have console without using seperate linker flags
# /LTCG allows for linking wholy optimizated programs
# /MANIFEST:NO disables generating a manifest file, we instead provide our own
# /STACK sets the stack reserve size, ATL's pack list needs 3-4 MiB as of November 2022, provide 8 MiB
set(CMAKE_EXE_LINKER_FLAGS "/LTCG /MANIFEST:NO /STACK:8388608 ${CMAKE_EXE_LINKER_FLAGS}")
# /GL enables whole program optimizations
# /Gw helps reduce binary size
# /Gy allows the compiler to package individual functions
# /guard:cf enables control flow guard
foreach(lang C CXX)
set("CMAKE_${lang}_FLAGS_RELEASE" "/GL /Gw /Gy /guard:cf")
endforeach()
# See https://github.com/ccache/ccache/issues/1040
# Note, CMake 3.25 replaces this with CMAKE_MSVC_DEBUG_INFORMATION_FORMAT
# See https://cmake.org/cmake/help/v3.25/variable/CMAKE_MSVC_DEBUG_INFORMATION_FORMAT.html
foreach(config DEBUG RELWITHDEBINFO)
foreach(lang C CXX)
set(flags_var "CMAKE_${lang}_FLAGS_${config}")
string(REGEX REPLACE "/Z[Ii]" "/Z7" ${flags_var} "${${flags_var}}")
endforeach()
endforeach()
if(CMAKE_MSVC_RUNTIME_LIBRARY STREQUAL "MultiThreadedDLL")
set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release "")
set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release "")
endif()
else()
set(CMAKE_CXX_FLAGS "-Wall -pedantic -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}")
# ATL's pack list needs more than the default 1 Mib stack on windows
if(WIN32)
set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}")
endif()
endif()
# Fix build with Qt 5.13
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00")
# Fix aarch64 build for toml++
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0")
# set CXXFLAGS for build targets
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}")
option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" on)
# If this is a Debug build turn on address sanitiser
if (CMAKE_BUILD_TYPE STREQUAL "Debug" AND DEBUG_ADDRESS_SANITIZER)
message(STATUS "Address Sanitizer enabled for Debug builds, Turn it off with -DDEBUG_ADDRESS_SANITIZER=off")
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
# using clang with clang-cl front end
message(STATUS "Address Sanitizer available on Clang MSVC frontend")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /O1 /Oy-")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /O1 /Oy-")
else()
# AppleClang and Clang
message(STATUS "Address Sanitizer available on Clang")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
endif()
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
# GCC
message(STATUS "Address Sanitizer available on GCC")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer")
link_libraries("asan")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
message(STATUS "Address Sanitizer available on MSVC")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /O1 /Oy-")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /O1 /Oy-")
else()
message(STATUS "Address Sanitizer not available on compiler ${CMAKE_CXX_COMPILER_ID}")
endif()
endif()
option(ENABLE_LTO "Enable Link Time Optimization" off)
if(ENABLE_LTO)
include(CheckIPOSupported)
check_ipo_supported(RESULT ipo_supported OUTPUT ipo_error)
if(ipo_supported AND (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel"))
message(STATUS "IPO / LTO enabled")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
elseif(ipo_supported)
message(STATUS "Not enabling IPO / LTO on debug builds")
if(ipo_supported)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_MINSIZEREL TRUE)
if(CMAKE_BUILD_TYPE)
if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
message(STATUS "IPO / LTO enabled")
else()
message(STATUS "Not enabling IPO / LTO on debug builds")
endif()
else()
message(STATUS "IPO / LTO will only be enabled for release builds")
endif()
else()
message(STATUS "IPO / LTO not supported: <${ipo_error}>")
endif()
@ -60,8 +142,20 @@ endif()
option(BUILD_TESTING "Build the testing tree." ON)
find_package(ECM REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH "${ECM_MODULE_PATH};${CMAKE_MODULE_PATH}")
find_package(ECM QUIET NO_MODULE)
if(NOT ECM_FOUND)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/extra-cmake-modules/CMakeLists.txt")
message(STATUS "Using bundled ECM")
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/libraries/extra-cmake-modules/modules;${CMAKE_MODULE_PATH}")
else()
message(FATAL_ERROR
" Could not find ECM\n \n"
" Either install ECM using the system package manager or clone submodules\n"
" Submodules can be cloned with 'git submodule update --init --recursive'")
endif()
else()
set(CMAKE_MODULE_PATH "${ECM_MODULE_PATH};${CMAKE_MODULE_PATH}")
endif()
include(CTest)
include(ECMAddTests)
if(BUILD_TESTING)
@ -76,7 +170,7 @@ set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL th
set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help")
######## Set version numbers ########
set(Launcher_VERSION_MAJOR 5)
set(Launcher_VERSION_MAJOR 8)
set(Launcher_VERSION_MINOR 0)
set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}")
@ -102,17 +196,17 @@ set(Launcher_BUG_TRACKER_URL "https://github.com/PrismLauncher/PrismLauncher/iss
set(Launcher_TRANSLATIONS_URL "https://hosted.weblate.org/projects/prismlauncher/launcher/" CACHE STRING "URL for the translations platform.")
# Matrix Space
set(Launcher_MATRIX_URL "https://matrix.to/#/#prismlauncher:matrix.org" CACHE STRING "URL to the Matrix Space")
set(Launcher_MATRIX_URL "https://prismlauncher.org/matrix" CACHE STRING "URL to the Matrix Space")
# Discord URL
set(Launcher_DISCORD_URL "https://discord.gg/prismlauncher" CACHE STRING "URL for the Discord guild.")
set(Launcher_DISCORD_URL "https://prismlauncher.org/discord" CACHE STRING "URL for the Discord guild.")
# Subreddit URL
set(Launcher_SUBREDDIT_URL "https://www.reddit.com/r/PrismLauncher/" CACHE STRING "URL for the subreddit.")
set(Launcher_SUBREDDIT_URL "https://prismlauncher.org/reddit" CACHE STRING "URL for the subreddit.")
# Builds
set(Launcher_FORCE_BUNDLED_LIBS OFF CACHE BOOL "Prevent using system libraries, if they are available as submodules")
set(Launcher_QT_VERSION_MAJOR "5" CACHE STRING "Major Qt version to build against")
set(Launcher_QT_VERSION_MAJOR "6" CACHE STRING "Major Qt version to build against")
# API Keys
# NOTE: These API keys are here for convenience. If you rebrand this software or intend to break the terms of service
@ -146,6 +240,16 @@ set(Launcher_BUILD_TIMESTAMP "${TODAY}")
################################ 3rd Party Libs ################################
# Successive configurations of cmake without cleaning the build dir will cause zlib fallback to fail due to cached values
# Record when fallback triggered and skip this find_package
if(NOT Launcher_FORCE_BUNDLED_LIBS AND NOT FORCE_BUNDLED_ZLIB)
find_package(ZLIB QUIET)
endif()
if(NOT ZLIB_FOUND)
set(FORCE_BUNDLED_ZLIB TRUE CACHE BOOL "")
mark_as_advanced(FORCE_BUNDLED_ZLIB)
endif()
# Find the required Qt parts
include(QtVersionlessBackport)
if(Launcher_QT_VERSION_MAJOR EQUAL 5)
@ -164,7 +268,7 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 5)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE")
elseif(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_VERSION_MAJOR 6)
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml Core5Compat)
find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat)
list(APPEND Launcher_QT_LIBS Qt6::Core5Compat)
if(NOT Launcher_FORCE_BUNDLED_LIBS)
@ -178,12 +282,16 @@ else()
message(FATAL_ERROR "Qt version ${Launcher_QT_VERSION_MAJOR} is not supported")
endif()
include(ECMQueryQt)
ecm_query_qt(QT_PLUGINS_DIR QT_INSTALL_PLUGINS)
ecm_query_qt(QT_LIBS_DIR QT_INSTALL_LIBS)
ecm_query_qt(QT_LIBEXECS_DIR QT_INSTALL_LIBEXECS)
ecm_query_qt(QT_DATA_DIR QT_HOST_DATA)
set(QT_MKSPECS_DIR ${QT_DATA_DIR}/mkspecs)
if(Launcher_QT_VERSION_MAJOR EQUAL 5)
include(ECMQueryQt)
ecm_query_qt(QT_PLUGINS_DIR QT_INSTALL_PLUGINS)
ecm_query_qt(QT_LIBS_DIR QT_INSTALL_LIBS)
ecm_query_qt(QT_LIBEXECS_DIR QT_INSTALL_LIBEXECS)
else()
set(QT_PLUGINS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_PLUGINS})
set(QT_LIBS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBS})
set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS})
endif()
# NOTE: Qt 6 already sets this by default
if (Qt5_POSITION_INDEPENDENT_CODE)
@ -196,8 +304,13 @@ if(NOT Launcher_FORCE_BUNDLED_LIBS)
# Find ghc_filesystem
find_package(ghc_filesystem QUIET)
# Find cmark
find_package(cmark QUIET)
endif()
include(ECMQtDeclareLoggingCategory)
####################################### Program Info #######################################
set(Launcher_APP_BINARY_NAME "prismlauncher" CACHE STRING "Name of the Launcher binary")
@ -222,14 +335,14 @@ if(UNIX AND APPLE)
set(APPS "\${CMAKE_INSTALL_PREFIX}/${Launcher_Name}.app")
# Mac bundle settings
set(MACOSX_BUNDLE_BUNDLE_NAME "${Launcher_Name}")
set(MACOSX_BUNDLE_INFO_STRING "${Launcher_Name}: A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.")
set(MACOSX_BUNDLE_BUNDLE_NAME "${Launcher_DisplayName}")
set(MACOSX_BUNDLE_INFO_STRING "${Launcher_DisplayName}: A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.")
set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.prismlauncher.${Launcher_Name}")
set(MACOSX_BUNDLE_BUNDLE_VERSION "${Launcher_VERSION_NAME}")
set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_NAME}")
set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_NAME}")
set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns)
set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2021-2022 ${Launcher_Copyright}")
set(MACOSX_BUNDLE_COPYRIGHT "© 2022 ${Launcher_Copyright_Mac}")
set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=")
set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml")
@ -247,13 +360,11 @@ if(UNIX AND APPLE)
install(FILES ${Launcher_Branding_ICNS} DESTINATION ${RESOURCES_DEST_DIR} RENAME ${Launcher_Name}.icns)
elseif(UNIX)
include(KDEInstallDirs)
set(BINARY_DEST_DIR "bin")
set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}")
set(JARS_DEST_DIR "share/${Launcher_APP_BINARY_NAME}")
set(LAUNCHER_DESKTOP_DEST_DIR "share/applications" CACHE STRING "Path to the desktop file directory")
set(LAUNCHER_METAINFO_DEST_DIR "share/metainfo" CACHE STRING "Path to the metainfo directory")
set(LAUNCHER_ICON_DEST_DIR "share/icons/hicolor/scalable/apps" CACHE STRING "Path to the scalable icon directory")
set(LAUNCHER_MAN_DEST_DIR "share/man/man6" CACHE STRING "Path to the man page directory")
set(JARS_DEST_DIR "share/${Launcher_Name}")
# install as bundle with no dependencies included
set(INSTALL_BUNDLE "nodeps")
@ -261,12 +372,15 @@ elseif(UNIX)
# Set RPATH
SET(Launcher_BINARY_RPATH "$ORIGIN/")
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${LAUNCHER_DESKTOP_DEST_DIR})
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${LAUNCHER_METAINFO_DEST_DIR})
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION ${LAUNCHER_ICON_DEST_DIR})
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${KDE_INSTALL_APPDIR})
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${KDE_INSTALL_METAINFODIR})
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps")
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR})
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}")
if(Launcher_ManPage)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION ${LAUNCHER_MAN_DEST_DIR})
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6")
endif()
# Install basic runner script if component "portable" is selected
@ -292,6 +406,8 @@ else()
message(FATAL_ERROR "Platform not supported")
endif()
################################ Included Libs ################################
include(ExternalProject)
@ -303,9 +419,34 @@ option(NBT_BUILD_TESTS "Build NBT library tests" OFF) #FIXME: fix unit tests.
add_subdirectory(libraries/libnbtplusplus)
add_subdirectory(libraries/systeminfo) # system information library
add_subdirectory(libraries/hoedown) # markdown parser
add_subdirectory(libraries/launcher) # java based launcher part for Minecraft
add_subdirectory(libraries/javacheck) # java compatibility checker
if(FORCE_BUNDLED_ZLIB)
message(STATUS "Using bundled zlib")
set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) # Suppress cmake warnings and allow INTERPROCEDURAL_OPTIMIZATION for zlib
set(SKIP_INSTALL_ALL ON)
add_subdirectory(libraries/zlib EXCLUDE_FROM_ALL)
# On OS where unistd.h exists, zlib's generated header defines `Z_HAVE_UNISTD_H`, while the included header does not.
# We cannot safely undo the rename on those systems, and they generally have packages for zlib anyway.
check_include_file(unistd.h NEED_GENERATED_ZCONF)
if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" AND NOT NEED_GENERATED_ZCONF)
# zlib's cmake script renames a file, dirtying the submodule, see https://github.com/madler/zlib/issues/162
message(STATUS "Undoing Rename")
message(STATUS " ${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h")
file(RENAME "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h")
endif()
set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" CACHE STRING "" FORCE)
set_target_properties(zlibstatic PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIR}")
add_library(ZLIB::ZLIB ALIAS zlibstatic)
set(ZLIB_LIBRARY ZLIB::ZLIB CACHE STRING "zlib library name")
find_package(ZLIB REQUIRED)
else()
message(STATUS "Using system zlib")
endif()
if (FORCE_BUNDLED_QUAZIP)
message(STATUS "Using bundled QuaZip")
set(BUILD_SHARED_LIBS 0) # link statically to avoid conflicts.
@ -322,17 +463,26 @@ if(NOT tomlplusplus_FOUND)
else()
message(STATUS "Using system tomlplusplus")
endif()
if(NOT cmark_FOUND)
message(STATUS "Using bundled cmark")
set(CMARK_STATIC ON CACHE BOOL "Build static libcmark library" FORCE)
set(CMARK_SHARED OFF CACHE BOOL "Build shared libcmark library" FORCE)
set(CMARK_TESTS OFF CACHE BOOL "Build cmark tests and enable testing" FORCE)
add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser
add_library(cmark::cmark ALIAS cmark_static)
else()
message(STATUS "Using system cmark")
endif()
add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much
add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
if (NOT ghc_filesystem_FOUND)
message(STATUS "Using bundled ghc_filesystem")
set(GHC_FILESYSTEM_WITH_INSTALL OFF) # Workaround ghc::filesystem bug
add_subdirectory(libraries/filesystem) # Implementation of std::filesystem for old C++, for usage in old macOS
add_library(ghcFilesystem::ghc_filesystem ALIAS ghc_filesystem)
else()
message(STATUS "Using system ghc_filesystem")
endif()
add_subdirectory(libraries/qdcss) # css parser
############################### Built Artifacts ###############################

View File

@ -1,7 +1,7 @@
## Prism Launcher
Prism Launcher - Minecraft Launcher
Copyright (C) 2022 Prism Launcher Contributors
Copyright (C) 2022-2023 Prism Launcher Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -156,23 +156,34 @@
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
## Hoedown
## cmark
Copyright (c) 2008, Natacha Porté
Copyright (c) 2011, Vicent Martí
Copyright (c) 2014, Xavier Mendez, Devin Torres and the Hoedown authors
Copyright (c) 2014, John MacFarlane
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
All rights reserved.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## Batch icon set
@ -398,3 +409,45 @@
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## Breeze icons
Copyright (C) 2014 Uri Herrera <uri_herrera@nitrux.in> and others
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
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/>.
## Oxygen Icons
The Oxygen Icon Theme
Copyright (C) 2007 Nuno Pinheiro <nuno@oxygen-icons.org>
Copyright (C) 2007 David Vignoni <david@icon-king.com>
Copyright (C) 2007 David Miller <miller@oxygen-icons.org>
Copyright (C) 2007 Johann Ollivier Lapeyre <johann@oxygen-icons.org>
Copyright (C) 2007 Kenneth Wimer <kwwii@bootsplash.org>
Copyright (C) 2007 Riccardo Iaconelli <riccardo@oxygen-icons.org>
and others
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
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/>.

102
README.md
View File

@ -1,49 +1,85 @@
<p align="center">
<img src="./program_info/org.prismlauncher.PrismLauncher.logo.svg#gh-light-mode-only" alt="Prism Launcher logo" width="50%"/>
<img src="./program_info/org.prismlauncher.PrismLauncher.logo-darkmode.svg#gh-dark-mode-only" alt="Prism Launcher logo" width="50%"/>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="/program_info/org.prismlauncher.PrismLauncher.logo-darkmode.svg">
<source media="(prefers-color-scheme: light)" srcset="/program_info/org.prismlauncher.PrismLauncher.logo.svg">
<img alt="Prism Launcher" src="/program_info/org.prismlauncher.PrismLauncher.logo.svg" width="40%">
</picture>
</p>
Prism Launcher is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.
This is a **fork** of the MultiMC Launcher and not endorsed by MultiMC.
<p align="center">
Prism Launcher is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.<br />
<br />This is a <b>fork</b> of the MultiMC Launcher and is <b>not</b> endorsed by it.
</p>
## Installation
- All downloads and instructions for Prism Launcher can be found [on our website](https://prismlauncher.org/download/).
- Last build status can be found [here](https://github.com/PrismLauncher/PrismLauncher/actions).
<a href="https://repology.org/project/prismlauncher/versions">
<img src="https://repology.org/badge/vertical-allrepos/prismlauncher.svg" alt="Packaging status" align="right">
</a>
- All downloads and instructions for Prism Launcher can be found on our [Website](https://prismlauncher.org/download).
- Last build status can be found in the [GitHub Actions](https://github.com/PrismLauncher/PrismLauncher/actions).
### Development Builds
There are development builds available [here](https://github.com/PrismLauncher/PrismLauncher/actions). These have debug information in the binaries, so their file sizes are relatively larger.
Portable builds are provided for on Linux, Windows, and macOS.
Prebuilt Development builds are provided for **Linux**, **Windows** and **macOS**.
For Debian and Arch, you can use these packages for the latest development versions:
[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-blue)](https://aur.archlinux.org/packages/prismlauncher-git/)
[![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-orange)](https://mpr.makedeb.org/packages/prismlauncher-git)
## Help & Support
For **Arch**, **Debian**, **Fedora**, **OpenSUSE (Tumbleweed)** and **Gentoo**, respectively, you can use these packages for the latest development versions:
Feel free to create an issue if you need help. However, you might find it easier to ask in the Discord server.
[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-1793D1?label=AUR&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-git) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-1793D1?label=AUR&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-qt5-git) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-A80030?label=MPR&logo=debian&logoColor=white)](https://mpr.makedeb.org/packages/prismlauncher-git)<br />[![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-51A2DA?label=COPR&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-nightly](https://img.shields.io/badge/OBS-prismlauncher--nightly-3AB6A9?logo=opensuse&logoColor=white)](https://build.opensuse.org/project/show/home:getchoo) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-4D4270?label=Gentoo&logo=gentoo&logoColor=white)](https://packages.gentoo.org/packages/games-action/prismlauncher)
[![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner3)](https://discord.gg/prismlauncher)
These packages are also availiable to all the distributions based on the ones mentioned above.
We will also soon be opening up our Matrix channels.
You can already join our Matrix space:
## Community & Support
[![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?label=PrismLauncher%20space)](https://matrix.to/#/#prismlauncher:matrix.org)
Feel free to create a GitHub issue if you find a bug or want to suggest a new feature. We have multiple community spaces where other community members can help you:
We also have a subreddit you can post your issues and suggestions on:
- **Our Discord server:**
[r/PrismLauncher](https://www.reddit.com/r/PrismLauncher/)
[![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner3)](https://prismlauncher.org/discord)
## Building
- **Our Matrix space:**
If you want to build Prism Launcher yourself, check [Build Instructions](https://prismlauncher.org/wiki/development/build-instructions/) for build instructions.
[![PrismLauncher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge&label=Matrix%20Space&logo=matrix&color=purple)](https://prismlauncher.org/matrix)
- **Our Subreddit:**
[![r/PrismLauncher](https://img.shields.io/reddit/subreddit-subscribers/prismlauncher?style=for-the-badge&logo=reddit)](https://prismlauncher.org/reddit)
## Translations
The translation effort for PrismLauncher is hosted on [Weblate](https://hosted.weblate.org/projects/prismlauncher/launcher/) and information about translating Prism Launcher is available at <https://github.com/PrismLauncher/Translations>
## Building
If you want to build Prism Launcher yourself, check the [Build Instructions](https://prismlauncher.org/wiki/development/build-instructions/).
## Sponsors & Partners
We thank all the wonderful backers over at Open Collective! Support Prism Launcher by [becoming a backer](https://opencollective.com/prismlauncher).
[![OpenCollective Backers](https://opencollective.com/prismlauncher/backers.svg?width=890&limit=1000)](https://opencollective.com/prismlauncher#backers)
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/)
Thanks to Weblate for hosting our translation efforts.
<a href="https://hosted.weblate.org/engage/prismlauncher/">
<img src="https://hosted.weblate.org/widgets/prismlauncher/-/open-graph.png" alt="Translation status" width="300" />
</a>
Thanks to Netlify for providing us their excellent web services, as part of their [Open Source program](https://www.netlify.com/open-source/).
<a href="https://www.netlify.com"> <img src="https://www.netlify.com/v3/img/components/netlify-color-accent.svg" alt="Deploys by Netlify" /> </a>
Thanks to the awesome people over at [MacStadium](https://www.macstadium.com/), for providing M1-Macs for development purposes!
<a href="https://www.macstadium.com"><img src="https://uploads-ssl.webflow.com/5ac3c046c82724970fc60918/5c019d917bba312af7553b49_MacStadium-developerlogo.png" alt="Powered by MacStadium" width="300"></a>
## Forking/Redistributing/Custom builds policy
We don't care what you do with your fork/custom build as long as you follow the terms of the [license](LICENSE) (this is a legal responsibility), and if you made code changes rather than just packaging a custom build, please do the following as a basic courtesy:
@ -60,28 +96,8 @@ Be aware that if you build this software without removing the provided API keys
If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file by setting them to an empty string (`""`).
## License
## License [![https://github.com/PrismLauncher/PrismLauncher/blob/develop/LICENSE](https://img.shields.io/github/license/PrismLauncher/PrismLauncher?label=License&logo=gnu&color=C4282D)](LICENSE)
All launcher code is available under the GPL-3.0-only license.
The logo and related assets are under the CC BY-SA 4.0 license.
## Sponsors
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/)
Thanks to Weblate for hosting our translation efforts.
<a href="https://hosted.weblate.org/engage/prismlauncher/">
<img src="https://hosted.weblate.org/widgets/prismlauncher/-/open-graph.png" alt="Translation status" width="300" />
</a>
Thanks to Netlify for providing us their excellent web services, as part of their [Open Source program](https://www.netlify.com/open-source/)
<a href="https://www.netlify.com"> <img src="https://www.netlify.com/v3/img/components/netlify-color-accent.svg" alt="Deploys by Netlify" /> </a>
Thanks to the awesome people over at [MacStadium](https://www.macstadium.com/), for providing M1-Macs for development purposes!
<a href="https://www.macstadium.com"><img src="https://uploads-ssl.webflow.com/5ac3c046c82724970fc60918/5c019d917bba312af7553b49_MacStadium-developerlogo.png" alt="Powered by MacStadium" width="300"></a>

View File

@ -49,6 +49,7 @@ Config::Config()
LAUNCHER_CONFIGFILE = "@Launcher_ConfigFile@";
LAUNCHER_GIT = "@Launcher_Git@";
LAUNCHER_DESKTOPFILENAME = "@Launcher_DesktopFileName@";
LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@";
USER_AGENT = "@Launcher_UserAgent@";
USER_AGENT_UNCACHED = USER_AGENT + " (Uncached)";
@ -75,10 +76,13 @@ Config::Config()
// Assume that builds outside of Git repos are "stable"
if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND")
|| GIT_TAG == QStringLiteral("GITDIR-NOTFOUND"))
|| GIT_TAG == QStringLiteral("GITDIR-NOTFOUND")
|| GIT_REFSPEC == QStringLiteral("")
|| GIT_TAG == QStringLiteral("GIT-NOTFOUND"))
{
GIT_REFSPEC = "refs/heads/stable";
GIT_TAG = versionString();
GIT_COMMIT = "";
}
if (GIT_REFSPEC.startsWith("refs/heads/"))

View File

@ -1,8 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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
@ -36,6 +37,7 @@
#pragma once
#include <QString>
#include <QList>
/**
* \brief The Config class holds all the build-time information passed from the build system.
@ -51,6 +53,7 @@ class Config {
QString LAUNCHER_CONFIGFILE;
QString LAUNCHER_GIT;
QString LAUNCHER_DESKTOPFILENAME;
QString LAUNCHER_SVGFILENAME;
/// The major version number.
int VERSION_MAJOR;
@ -159,6 +162,9 @@ class Config {
QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2";
QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2";
QStringList MODRINTH_MRPACK_HOSTS{"cdn.modrinth.com", "github.com", "raw.githubusercontent.com", "gitlab.com"};
QString FLAME_BASE_URL = "https://api.curseforge.com/v1";
QString versionString() const;
/**

View File

@ -44,5 +44,28 @@
<string>${MACOSX_SPARKLE_UPDATE_PUBLIC_KEY}</string>
<key>SUFeedURL</key>
<string>${MACOSX_SPARKLE_UPDATE_FEED_URL}</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>zip</string>
<string>mrpack</string>
</array>
<key>CFBundleTypeName</key>
<string>Prism Launcher instance</string>
<key>CFBundleTypeOSTypes</key>
<array>
<string>TEXT</string>
<string>utxt</string>
<string>TUTX</string>
<string>****</string>
</array>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
</dict>
</array>
</dict>
</plist>

View File

@ -1 +1,14 @@
(import nix/flake-compat.nix).defaultNix
(
import
(
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
in
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash;
}
)
{src = ./.;}
)
.defaultNix

149
flake.lock generated
View File

@ -3,11 +3,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1650374568,
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
@ -16,6 +16,63 @@
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1688254665,
"narHash": "sha256-8FHEgBrr7gYNiS/NzCxIO3m4hvtLRW9YY1nYo1ivm3o=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "267149c58a14d15f7f81b4d737308421de9d7152",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1685518550,
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1660459072,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"libnbtplusplus": {
"flake": false,
"locked": {
@ -34,11 +91,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1666057921,
"narHash": "sha256-VpQqtXdj6G7cH//SvoprjR7XT3KS7p+tCVebGK1N6tE=",
"lastModified": 1688221086,
"narHash": "sha256-cdW6qUL71cNWhHCpMPOJjlw0wzSRP0pVlRn2vqX/VVg=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "88eab1e431cabd0ed621428d8b40d425a07af39f",
"rev": "cd99c2b3c9f160cd004318e0697f90bbd5960825",
"type": "github"
},
"original": {
@ -48,27 +105,73 @@
"type": "github"
}
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"libnbtplusplus": "libnbtplusplus",
"nixpkgs": "nixpkgs",
"tomlplusplus": "tomlplusplus"
}
},
"tomlplusplus": {
"flake": false,
"nixpkgs-lib": {
"locked": {
"lastModified": 1666091090,
"narHash": "sha256-djpMCFPvkJcfynV8WnsYdtwLq+J7jpV1iM4C6TojiyM=",
"owner": "marzer",
"repo": "tomlplusplus",
"rev": "1e4a3833d013aee08f58c5b31c69f709afc69f73",
"dir": "lib",
"lastModified": 1688049487,
"narHash": "sha256-100g4iaKC9MalDjUW9iN6Jl/OocTDtXdeAj7pEGIRh4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4bc72cae107788bf3f24f30db2e2f685c9298dc9",
"type": "github"
},
"original": {
"owner": "marzer",
"repo": "tomlplusplus",
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": [
"flake-compat"
],
"flake-utils": "flake-utils",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-stable": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1688386108,
"narHash": "sha256-Vffto9QaVonzYAcPlAzd0soqWYpPpKk60dfNLSIXcFA=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "42587d3414d1747999a5f71e92a83cf6547b62da",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"libnbtplusplus": "libnbtplusplus",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}

View File

@ -3,36 +3,25 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-compat = { url = "github:edolstra/flake-compat"; flake = false; };
libnbtplusplus = { url = "github:PrismLauncher/libnbtplusplus"; flake = false; };
tomlplusplus = { url = "github:marzer/tomlplusplus"; flake = false; };
flake-parts.url = "github:hercules-ci/flake-parts";
pre-commit-hooks = {
url = "github:cachix/pre-commit-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs";
inputs.nixpkgs-stable.follows = "nixpkgs";
inputs.flake-compat.follows = "flake-compat";
};
flake-compat = {
url = "github:edolstra/flake-compat";
flake = false;
};
libnbtplusplus = {
url = "github:PrismLauncher/libnbtplusplus";
flake = false;
};
};
outputs = { self, nixpkgs, libnbtplusplus, tomlplusplus, ... }:
let
# User-friendly version number.
version = builtins.substring 0 8 self.lastModifiedDate;
# Supported systems (qtbase is currently broken for "aarch64-darwin")
supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" ];
# Helper function to generate an attrset '{ x86_64-linux = f "x86_64-linux"; ... }'.
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
# Nixpkgs instantiated for supported systems.
pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system});
packagesFn = pkgs: rec {
prismlauncher = pkgs.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus tomlplusplus; };
prismlauncher-qt6 = pkgs.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus tomlplusplus; };
};
in
{
packages = forAllSystems (system:
let packages = packagesFn pkgs.${system}; in
packages // { default = packages.prismlauncher; }
);
overlay = final: packagesFn;
};
outputs = inputs:
inputs.flake-parts.lib.mkFlake
{inherit inputs;}
{imports = [./nix];};
}

View File

@ -0,0 +1,85 @@
id: org.prismlauncher.PrismLauncher
runtime: org.kde.Platform
runtime-version: "5.15-22.08"
sdk: org.kde.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.openjdk17
- org.freedesktop.Sdk.Extension.openjdk8
add-extensions:
com.valvesoftware.Steam.Utility.gamescope:
version: stable
add-ld-path: lib
no-autodownload: true
autodelete: false
directory: utils/gamescope
command: prismlauncher
finish-args:
- --share=ipc
- --socket=x11
- --socket=wayland
- --device=all
- --share=network
- --socket=pulseaudio
# for Discord RPC mods
- --filesystem=xdg-run/app/com.discordapp.Discord:create
# Mod drag&drop
- --filesystem=xdg-download:ro
modules:
- name: prismlauncher
buildsystem: cmake-ninja
config-opts:
- -DLauncher_BUILD_PLATFORM=flatpak
- -DCMAKE_BUILD_TYPE=Debug
- -DLauncher_QT_VERSION_MAJOR=5
build-options:
env:
JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17
JAVA_COMPILER: /usr/lib/sdk/openjdk17/jvm/openjdk-17/bin/javac
sources:
- type: dir
path: ../
builddir: true
- name: openjdk
buildsystem: simple
build-commands:
- mkdir -p /app/jdk/
- /usr/lib/sdk/openjdk17/install.sh
- mv /app/jre /app/jdk/17
- /usr/lib/sdk/openjdk8/install.sh
- mv /app/jre /app/jdk/8
cleanup: [/jre]
- name: xrandr
buildsystem: autotools
sources:
- type: archive
url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.1.tar.xz
sha256: 7bc76daf9d72f8aff885efad04ce06b90488a1a169d118dea8a2b661832e8762
cleanup: [/share/man, /bin/xkeystone]
- name: gamemode
buildsystem: meson
config-opts:
- -Dwith-sd-bus-provider=no-daemon
- -Dwith-examples=false
post-install:
# gamemoderun is installed for users who want to use wrapper commands
# post-install is running inside the build dir, we need it from the source though
- install -Dm755 ../data/gamemoderun -t /app/bin
sources:
- type: git
url: https://github.com/FeralInteractive/gamemode
tag: "1.7"
commit: 4dc99dff76218718763a6b07fc1900fa6d1dafd9
- name: enhance
buildsystem: simple
build-commands:
- mkdir -p /app/utils/gamescope
- install -Dm755 prime-run /app/bin/prime-run
- mv /app/bin/prismlauncher /app/bin/prismrun
- install -Dm755 prismlauncher /app/bin/prismlauncher
sources:
- type: file
path: ../flatpak/prime-run
- type: file
path: ../flatpak/prismlauncher

4
flatpak/prime-run Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
export __NV_PRIME_RENDER_OFFLOAD=1 __VK_LAYER_NV_optimus=NVIDIA_only __GLX_VENDOR_LIBRARY_NAME=nvidia
exec "$@"

11
flatpak/prismlauncher Normal file
View File

@ -0,0 +1,11 @@
#!/bin/bash
# discord RPC
for i in {0..9}; do
test -S "$XDG_RUNTIME_DIR"/discord-ipc-"$i" || ln -sf {app/com.discordapp.Discord,"$XDG_RUNTIME_DIR"}/discord-ipc-"$i";
done
export PATH="${PATH}${PATH:+:}/app/utils/gamescope/bin:/usr/lib/extensions/vulkan/MangoHud/bin"
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}${LD_LIBRARY_PATH:+:}/usr/lib/extensions/vulkan/MangoHud/\$LIB/"
exec /app/bin/prismrun "$@"

View File

@ -1,8 +1,14 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net>
//
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 Lenny McLennington <lenny@sneed.church>
* Copyright (C) 2022 Tayou <tayou@gmx.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -37,10 +43,15 @@
#include "Application.h"
#include "BuildConfig.h"
#include "DataMigrationTask.h"
#include "net/PasteUpload.h"
#include "pathmatcher/MultiMatcher.h"
#include "pathmatcher/SimplePrefixMatcher.h"
#include "settings/INIFile.h"
#include "ui/MainWindow.h"
#include "ui/InstanceWindow.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui/instanceview/AccessibleInstanceView.h"
#include "ui/pages/BasePageProvider.h"
@ -54,29 +65,24 @@
#include "ui/pages/global/APIPage.h"
#include "ui/pages/global/CustomCommandsPage.h"
#include "ui/themes/ITheme.h"
#include "ui/themes/SystemTheme.h"
#include "ui/themes/DarkTheme.h"
#include "ui/themes/BrightTheme.h"
#include "ui/themes/CustomTheme.h"
#ifdef Q_OS_WIN
#include "ui/WinDarkmode.h"
#endif
#include "ui/setupwizard/SetupWizard.h"
#include "ui/setupwizard/LanguageWizardPage.h"
#include "ui/setupwizard/JavaWizardPage.h"
#include "ui/setupwizard/PasteWizardPage.h"
#include "ui/setupwizard/ThemeWizardPage.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/pagedialog/PageDialog.h"
#include "ui/themes/ThemeManager.h"
#include "ApplicationMessage.h"
#include <iostream>
#include <mutex>
#include <QFileOpenEvent>
#include <QAccessible>
#include <QCommandLineParser>
#include <QDir>
@ -92,6 +98,7 @@
#include <QIcon>
#include "InstanceList.h"
#include "MTPixmapCache.h"
#include <minecraft/auth/AccountList.h>
#include "icons/IconList.h"
@ -99,7 +106,7 @@
#include "java/JavaUtils.h"
#include "updater/UpdateChecker.h"
#include "updater/ExternalUpdater.h"
#include "tools/JProfiler.h"
#include "tools/JVisualVM.h"
@ -120,6 +127,11 @@
#ifdef Q_OS_LINUX
#include <dlfcn.h>
#include "gamemode_client.h"
#include "MangoHud.h"
#endif
#ifdef Q_OS_MAC
#include "updater/MacSparkleUpdater.h"
#endif
@ -136,20 +148,18 @@
static const QLatin1String liveCheckFile("live.check");
PixmapCache* PixmapCache::s_instance = nullptr;
namespace {
/** This is used so that we can output to the log file in addition to the CLI. */
void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
const char *levels = "DWCFIS";
const QString format("%1 %2 %3\n");
static std::mutex loggerMutex;
const std::lock_guard<std::mutex> lock(loggerMutex); // synchronized, QFile logFile is not thread-safe
qint64 msecstotal = APPLICATION->timeSinceStart();
qint64 seconds = msecstotal / 1000;
qint64 msecs = msecstotal % 1000;
QString foo;
char buf[1025] = {0};
::snprintf(buf, 1024, "%5lld.%03lld", seconds, msecs);
QString out = format.arg(buf).arg(levels[type]).arg(msg);
QString out = qFormatLogMessage(type, context, msg);
out += QChar::LineFeed;
APPLICATION->logFile->write(out.toUtf8());
APPLICATION->logFile->flush();
@ -157,45 +167,6 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QSt
fflush(stderr);
}
QString getIdealPlatform(QString currentPlatform) {
auto info = Sys::getKernelInfo();
switch(info.kernelType) {
case Sys::KernelType::Darwin: {
if(info.kernelMajor >= 17) {
// macOS 10.13 or newer
return "osx64-5.15.2";
}
else {
// macOS 10.12 or older
return "osx64";
}
}
case Sys::KernelType::Windows: {
// FIXME: 5.15.2 is not stable on Windows, due to a large number of completely unpredictable and hard to reproduce issues
break;
/*
if(info.kernelMajor == 6 && info.kernelMinor >= 1) {
// Windows 7
return "win32-5.15.2";
}
else if (info.kernelMajor > 6) {
// Above Windows 7
return "win32-5.15.2";
}
else {
// Below Windows 7
return "win32";
}
*/
}
case Sys::KernelType::Undetermined:
case Sys::KernelType::Linux: {
break;
}
}
return currentPlatform;
}
}
Application::Application(int &argc, char **argv) : QApplication(argc, argv)
@ -228,7 +199,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN);
setApplicationName(BuildConfig.LAUNCHER_NAME);
setApplicationDisplayName(QString("%1 %2").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()));
setApplicationVersion(BuildConfig.printableVersionString());
setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT);
setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME);
startTime = QDateTime::currentDateTime();
@ -245,7 +216,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
{{"s", "server"}, "Join the specified server on launch (only valid in combination with --launch)", "address"},
{{"a", "profile"}, "Use the account specified by its profile name (only valid in combination with --launch)", "profile"},
{"alive", "Write a small '" + liveCheckFile + "' file after the launcher starts"},
{{"I", "import"}, "Import instance from specified zip (local path or URL)", "file"}
{{"I", "import"}, "Import instance from specified zip (local path or URL)", "file"},
{"show", "Opens the window for the specified instance (by instance ID)", "show"}
});
parser.addHelpOption();
parser.addVersionOption();
@ -256,7 +228,18 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_serverToJoin = parser.value("server");
m_profileToUse = parser.value("profile");
m_liveCheck = parser.isSet("alive");
m_zipToImport = parser.value("import");
m_instanceIdToShowWindowOf = parser.value("show");
for (auto zip_path : parser.values("import")){
m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath()));
}
// treat unspecified positional arguments as import urls
for (auto zip_path : parser.positionalArguments()) {
m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath()));
}
// error if --launch is missing with --server or --profile
if((!m_serverToJoin.isEmpty() || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty())
@ -301,26 +284,11 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
dataPath = foo.absolutePath();
adjustedBy = "Persistent data path";
QDir polymcData(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation), "PolyMC"));
if (polymcData.exists()) {
dataPath = polymcData.absolutePath();
adjustedBy = "PolyMC data path";
}
#ifdef Q_OS_LINUX
// TODO: this should be removed in a future version
// TODO: provide a migration path similar to macOS migration
QDir bar(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation), "polymc"));
if (bar.exists()) {
dataPath = bar.absolutePath();
adjustedBy = "Legacy data path";
}
#endif
#ifndef Q_OS_MACOS
if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
dataPath = m_rootPath;
adjustedBy = "Portable data path";
m_portable = true;
}
#endif
}
@ -357,7 +325,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
}
/*
* Establish the mechanism for communication with an already running PolyMC that uses the same data path.
* Establish the mechanism for communication with an already running PrismLauncher that uses the same data path.
* If there is one, tell it what the user actually wanted to do and exit.
* We want to initialize this before logging to avoid messing with the log of a potential already running copy.
*/
@ -375,12 +343,14 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
activate.command = "activate";
m_peerInstance->sendMessage(activate.serialize(), timeout);
if(!m_zipToImport.isEmpty())
if(!m_zipsToImport.isEmpty())
{
ApplicationMessage import;
import.command = "import";
import.args.insert("path", m_zipToImport.toString());
m_peerInstance->sendMessage(import.serialize(), timeout);
for (auto zip_url : m_zipsToImport) {
ApplicationMessage import;
import.command = "import";
import.args.insert("path", zip_url.toString());
m_peerInstance->sendMessage(import.serialize(), timeout);
}
}
}
else
@ -406,39 +376,101 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// init the logger
{
static const QString logBase = BuildConfig.LAUNCHER_NAME + "-%0.log";
auto moveFile = [](const QString &oldName, const QString &newName)
{
static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log";
static const QString logBase = FS::PathCombine("logs", baseLogFile);
auto moveFile = [](const QString& oldName, const QString& newName) {
QFile::remove(newName);
QFile::copy(oldName, newName);
QFile::remove(oldName);
};
if (FS::ensureFolderPathExists("logs")) { // if this did not fail
for (auto i = 0; i <= 4; i++)
if (auto oldName = baseLogFile.arg(i);
QFile::exists(oldName)) // do not pointlessly delete new files if the old ones are not there
moveFile(oldName, logBase.arg(i));
}
moveFile(logBase.arg(3), logBase.arg(4));
moveFile(logBase.arg(2), logBase.arg(3));
moveFile(logBase.arg(1), logBase.arg(2));
moveFile(logBase.arg(0), logBase.arg(1));
for (auto i = 4; i > 0; i--)
moveFile(logBase.arg(i - 1), logBase.arg(i));
logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0)));
if(!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate))
{
showFatalErrorMessage(
"The launcher data folder is not writable!",
QString(
"The launcher couldn't create a log file - the data folder is not writable.\n"
"\n"
"Make sure you have write permissions to the data folder.\n"
"(%1)\n"
"\n"
"The launcher cannot continue until you fix this problem."
).arg(dataPath)
);
if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
showFatalErrorMessage("The launcher data folder is not writable!",
QString("The launcher couldn't create a log file - the data folder is not writable.\n"
"\n"
"Make sure you have write permissions to the data folder.\n"
"(%1)\n"
"\n"
"The launcher cannot continue until you fix this problem.")
.arg(dataPath));
return;
}
qInstallMessageHandler(appDebugOutput);
qSetMessagePattern(
"%{time process}" " "
"%{if-debug}D%{endif}" "%{if-info}I%{endif}" "%{if-warning}W%{endif}" "%{if-critical}C%{endif}" "%{if-fatal}F%{endif}"
" " "|" " "
"%{if-category}[%{category}]: %{endif}"
"%{message}");
bool foundLoggingRules = false;
auto logRulesFile = QStringLiteral("qtlogging.ini");
auto logRulesPath = FS::PathCombine(dataPath, logRulesFile);
qDebug() << "Testing" << logRulesPath << "...";
foundLoggingRules = QFile::exists(logRulesPath);
// search the dataPath()
// seach app data standard path
if(!foundLoggingRules && !isPortable() && dirParam.isEmpty()) {
logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile));
if(!logRulesPath.isEmpty()) {
qDebug() << "Found" << logRulesPath << "...";
foundLoggingRules = true;
}
}
// seach root path
if(!foundLoggingRules) {
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
logRulesPath = FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME, logRulesFile);
#else
logRulesPath = FS::PathCombine(m_rootPath, logRulesFile);
#endif
qDebug() << "Testing" << logRulesPath << "...";
foundLoggingRules = QFile::exists(logRulesPath);
}
if(foundLoggingRules) {
// load and set logging rules
qDebug() << "Loading logging rules from:" << logRulesPath;
QSettings loggingRules(logRulesPath, QSettings::IniFormat);
loggingRules.beginGroup("Rules");
QStringList rule_names = loggingRules.childKeys();
QStringList rules;
qDebug() << "Setting log rules:";
for (auto rule_name : rule_names) {
auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString());
rules.append(rule);
qDebug() << " " << rule;
}
auto rules_str = rules.join("\n");
QLoggingCategory::setFilterRules(rules_str);
}
qDebug() << "<> Log initialized.";
}
{
bool migrated = false;
if (!migrated)
migrated = handleDataMigration(dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC", "polymc.cfg");
if (!migrated)
migrated = handleDataMigration(dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), "MultiMC", "multimc.cfg");
}
{
qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT;
@ -490,14 +522,11 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
{
// Provide a fallback for migration from PolyMC
m_settings.reset(new INISettingsObject({ BuildConfig.LAUNCHER_CONFIGFILE, "polymc.cfg", "multimc.cfg" }, this));
// Updates
// Multiple channels are separated by spaces
m_settings->registerSetting("UpdateChannel", BuildConfig.VERSION_CHANNEL);
m_settings->registerSetting("AutoUpdate", true);
// Theming
m_settings->registerSetting("IconTheme", QString("pe_colored"));
m_settings->registerSetting("ApplicationTheme", QString("system"));
m_settings->registerSetting("ApplicationTheme", QString());
m_settings->registerSetting("BackgroundCat", QString("kitteh"));
// Remembered state
m_settings->registerSetting("LastUsedGroupForNewInstance", QString());
@ -535,12 +564,15 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("InstanceDir", "instances");
m_settings->registerSetting({"CentralModsDir", "ModsDir"}, "mods");
m_settings->registerSetting("IconsDir", "icons");
m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
m_settings->registerSetting("DownloadsDirWatchRecursive", false);
// Editors
m_settings->registerSetting("JsonEditor", QString());
// Language
m_settings->registerSetting("Language", QString());
m_settings->registerSetting("UseSystemLocale", false);
// Console
m_settings->registerSetting("ShowConsole", false);
@ -562,12 +594,12 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// Memory
m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512);
m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 4096);
m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, suitableMaxMem());
m_settings->registerSetting("PermGen", 128);
// Java Settings
m_settings->registerSetting("JavaPath", "");
m_settings->registerSetting("JavaTimestamp", 0);
m_settings->registerSetting("JavaSignature", "");
m_settings->registerSetting("JavaArchitecture", "");
m_settings->registerSetting("JavaRealArchitecture", "");
m_settings->registerSetting("JavaVersion", "");
@ -610,6 +642,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// The cat
m_settings->registerSetting("TheCat", false);
m_settings->registerSetting("ToolbarsLocked", false);
m_settings->registerSetting("InstSortMode", "Name");
m_settings->registerSetting("SelectedInstance", QString());
@ -629,6 +663,9 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("UpdateDialogGeometry", "");
m_settings->registerSetting("ModDownloadGeometry", "");
m_settings->registerSetting("RPDownloadGeometry", "");
m_settings->registerSetting("TPDownloadGeometry", "");
m_settings->registerSetting("ShaderDownloadGeometry", "");
// HACK: This code feels so stupid is there a less stupid way of doing this?
{
@ -655,8 +692,16 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->reset("PastebinCustomAPIBase");
}
}
// meta URL
m_settings->registerSetting("MetaURLOverride", "");
{
// Meta URL
m_settings->registerSetting("MetaURLOverride", "");
QUrl metaUrl(m_settings->get("MetaURLOverride").toString());
// get rid of invalid meta urls
if (!metaUrl.isValid() || metaUrl.scheme() != "http" || metaUrl.scheme() != "https")
m_settings->reset("MetaURLOverride");
}
m_settings->registerSetting("CloseAfterLaunch", false);
m_settings->registerSetting("QuitAfterGameStop", false);
@ -675,6 +720,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->set("FlameKeyOverride", flameKey);
m_settings->reset("CFKeyOverride");
}
m_settings->registerSetting("ModrinthToken", "");
m_settings->registerSetting("UserAgentOverride", "");
// Init page provider
@ -690,6 +736,9 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_globalSettingsProvider->addPage<AccountListPage>();
m_globalSettingsProvider->addPage<APIPage>();
}
PixmapCache::setInstance(new PixmapCache(this));
qDebug() << "<> Settings loaded.";
}
@ -699,7 +748,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// initialize network access and proxy setup
{
m_network = new QNetworkAccessManager();
m_network.reset(new QNetworkAccessManager());
QString proxyTypeStr = settings()->get("ProxyType").toString();
QString addr = settings()->get("ProxyAddr").toString();
int port = settings()->get("ProxyPort").value<qint16>();
@ -721,10 +770,10 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
// initialize the updater
if(BuildConfig.UPDATER_ENABLED)
{
auto platform = getIdealPlatform(BuildConfig.BUILD_PLATFORM);
auto channelUrl = BuildConfig.UPDATER_BASE + platform + "/channels.json";
qDebug() << "Initializing updater with platform: " << platform << " -- " << channelUrl;
m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_CHANNEL));
qDebug() << "Initializing updater";
#ifdef Q_OS_MAC
m_updater.reset(new MacSparkleUpdater());
#endif
qDebug() << "<> Updater started.";
}
@ -746,29 +795,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
qDebug() << "<> Instance icons intialized.";
}
// Icon themes
{
// TODO: icon themes and instance icons do not mesh well together. Rearrange and fix discrepancies!
// set icon theme search path!
auto searchPaths = QIcon::themeSearchPaths();
searchPaths.append("iconthemes");
QIcon::setThemeSearchPaths(searchPaths);
qDebug() << "<> Icon themes initialized.";
}
// Initialize widget themes
{
auto insertTheme = [this](ITheme * theme)
{
m_themes.insert(std::make_pair(theme->id(), std::unique_ptr<ITheme>(theme)));
};
auto darkTheme = new DarkTheme();
insertTheme(new SystemTheme());
insertTheme(darkTheme);
insertTheme(new BrightTheme());
insertTheme(new CustomTheme(darkTheme, "custom"));
qDebug() << "<> Widget themes initialized.";
}
// Themes
m_themeManager = std::make_unique<ThemeManager>(m_mainWindow);
// initialize and load all instances
{
@ -860,12 +888,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
}
});
{
setIconTheme(settings()->get("IconTheme").toString());
qDebug() << "<> Icon theme set.";
setApplicationTheme(settings()->get("ApplicationTheme").toString(), true);
qDebug() << "<> Application theme set.";
}
applyCurrentlySelectedTheme(true);
updateCapabilities();
@ -900,14 +923,10 @@ bool Application::createSetupWizard()
}
return false;
}();
bool languageRequired = [&]()
{
if (settings()->get("Language").toString().isEmpty())
return true;
return false;
}();
bool languageRequired = settings()->get("Language").toString().isEmpty();
bool pasteInterventionRequired = settings()->get("PastebinURL") != "";
bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired;
bool themeInterventionRequired = settings()->get("ApplicationTheme") == "";
bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired;
if(wizardRequired)
{
@ -926,6 +945,12 @@ bool Application::createSetupWizard()
{
m_setupWizard->addPage(new PasteWizardPage(m_setupWizard));
}
if (themeInterventionRequired)
{
settings()->set("ApplicationTheme", QString("system")); // set default theme after going into theme wizard
m_setupWizard->addPage(new ThemeWizardPage(m_setupWizard));
}
connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished);
m_setupWizard->show();
return true;
@ -933,18 +958,24 @@ bool Application::createSetupWizard()
return false;
}
bool Application::event(QEvent* event) {
bool Application::event(QEvent* event)
{
#ifdef Q_OS_MACOS
if (event->type() == QEvent::ApplicationStateChange) {
auto ev = static_cast<QApplicationStateChangeEvent*>(event);
if (m_prevAppState == Qt::ApplicationActive
&& ev->applicationState() == Qt::ApplicationActive) {
if (m_prevAppState == Qt::ApplicationActive && ev->applicationState() == Qt::ApplicationActive) {
emit clickedOnDock();
}
m_prevAppState = ev->applicationState();
}
#endif
if (event->type() == QEvent::FileOpen) {
auto ev = static_cast<QFileOpenEvent*>(event);
m_mainWindow->processURLs({ ev->url() });
}
return QApplication::event(event);
}
@ -986,16 +1017,26 @@ void Application::performMainStartupAction()
return;
}
}
if(!m_instanceIdToShowWindowOf.isEmpty())
{
auto inst = instances()->getInstanceById(m_instanceIdToShowWindowOf);
if(inst)
{
qDebug() << "<> Showing window of instance " << m_instanceIdToShowWindowOf;
showInstanceWindow(inst);
return;
}
}
if(!m_mainWindow)
{
// normal main window
showMainWindow(false);
qDebug() << "<> Main window shown.";
}
if(!m_zipToImport.isEmpty())
if(!m_zipsToImport.isEmpty())
{
qDebug() << "<> Importing instance from zip:" << m_zipToImport;
m_mainWindow->droppedURLs({ m_zipToImport });
qDebug() << "<> Importing from zip:" << m_zipsToImport;
m_mainWindow->processURLs( m_zipsToImport );
}
}
@ -1048,7 +1089,7 @@ void Application::messageReceived(const QByteArray& message)
qWarning() << "Received" << command << "message without a zip path/URL.";
return;
}
m_mainWindow->droppedURLs({ QUrl(path) });
m_mainWindow->processURLs({ QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath()) });
}
else if(command == "launch")
{
@ -1112,60 +1153,30 @@ std::shared_ptr<JavaInstallList> Application::javalist()
return m_javalist;
}
std::vector<ITheme *> Application::getValidApplicationThemes()
QList<ITheme*> Application::getValidApplicationThemes()
{
std::vector<ITheme *> ret;
auto iter = m_themes.cbegin();
while (iter != m_themes.cend())
{
ret.push_back((*iter).second.get());
iter++;
}
return ret;
return m_themeManager->getValidApplicationThemes();
}
bool Application::isFlatpak()
void Application::applyCurrentlySelectedTheme(bool initial)
{
#ifdef Q_OS_LINUX
return QFile::exists("/.flatpak-info");
#else
return false;
#endif
m_themeManager->applyCurrentlySelectedTheme(initial);
}
void Application::setApplicationTheme(const QString& name, bool initial)
void Application::setApplicationTheme(const QString& name)
{
auto systemPalette = qApp->palette();
auto themeIter = m_themes.find(name);
if(themeIter != m_themes.end())
{
auto & theme = (*themeIter).second;
theme->apply(initial);
#ifdef Q_OS_WIN
if (m_mainWindow) {
if (QString::compare(theme->id(), "dark") == 0) {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true);
} else {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false);
}
}
#endif
}
else
{
qWarning() << "Tried to set invalid theme:" << name;
}
m_themeManager->setApplicationTheme(name);
}
void Application::setIconTheme(const QString& name)
{
QIcon::setThemeName(name);
m_themeManager->setIconTheme(name);
}
QIcon Application::getThemedIcon(const QString& name)
{
if(name == "logo") {
return QIcon(":/org.prismlauncher.PrismLauncher.svg"); // FIXME: Make this a BuildConfig variable
return QIcon(":/" + BuildConfig.LAUNCHER_SVGFILENAME);
}
return QIcon::fromTheme(name);
}
@ -1382,13 +1393,7 @@ MainWindow* Application::showMainWindow(bool minimized)
m_mainWindow = new MainWindow();
m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toByteArray()));
m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray()));
#ifdef Q_OS_WIN
if (QString::compare(settings()->get("ApplicationTheme").toString(), "dark") == 0) {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true);
} else {
WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false);
}
#endif
if(minimized)
{
m_mainWindow->showMinimized();
@ -1553,17 +1558,8 @@ void Application::updateCapabilities()
if (gamemode_query_status() >= 0)
m_capabilities |= SupportsGameMode;
{
void *dummy = dlopen("libMangoHud_dlsym.so", RTLD_LAZY);
// try normal variant as well
if (dummy == NULL)
dummy = dlopen("libMangoHud.so", RTLD_LAZY);
if (dummy != NULL) {
dlclose(dummy);
m_capabilities |= SupportsMangoHud;
}
}
if (!MangoHud::getLibraryString().isEmpty())
m_capabilities |= SupportsMangoHud;
#endif
}
@ -1571,10 +1567,11 @@ QString Application::getJarPath(QString jarFile)
{
QStringList potentialPaths = {
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
FS::PathCombine(m_rootPath, "share/" + BuildConfig.LAUNCHER_APP_BINARY_NAME),
FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME),
#endif
FS::PathCombine(m_rootPath, "jars"),
FS::PathCombine(applicationDirPath(), "jars")
FS::PathCombine(applicationDirPath(), "jars"),
FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir, for debuging
};
for(QString p : potentialPaths)
{
@ -1605,6 +1602,15 @@ QString Application::getFlameAPIKey()
return BuildConfig.FLAME_API_KEY;
}
QString Application::getModrinthAPIToken()
{
QString tokenOverride = m_settings->get("ModrinthToken").toString();
if (!tokenOverride.isEmpty())
return tokenOverride;
return QString();
}
QString Application::getUserAgent()
{
QString uaOverride = m_settings->get("UserAgentOverride").toString();
@ -1625,3 +1631,114 @@ QString Application::getUserAgentUncached()
return BuildConfig.USER_AGENT_UNCACHED;
}
int Application::suitableMaxMem()
{
float totalRAM = (float)Sys::getSystemRam() / (float)Sys::mebibyte;
int maxMemoryAlloc;
// If totalRAM < 6GB, use (totalRAM / 1.5), else 4GB
if (totalRAM < (4096 * 1.5))
maxMemoryAlloc = (int) (totalRAM / 1.5);
else
maxMemoryAlloc = 4096;
return maxMemoryAlloc;
}
bool Application::handleDataMigration(const QString& currentData,
const QString& oldData,
const QString& name,
const QString& configFile) const
{
QString nomigratePath = FS::PathCombine(currentData, name + "_nomigrate.txt");
QStringList configPaths = { FS::PathCombine(oldData, configFile), FS::PathCombine(oldData, BuildConfig.LAUNCHER_CONFIGFILE) };
QLocale locale;
// Is there a valid config at the old location?
bool configExists = false;
for (QString configPath : configPaths) {
configExists |= QFileInfo::exists(configPath);
}
if (!configExists || QFileInfo::exists(nomigratePath)) {
qDebug() << "<> No migration needed from" << name;
return false;
}
QString message;
bool currentExists = QFileInfo::exists(FS::PathCombine(currentData, BuildConfig.LAUNCHER_CONFIGFILE));
if (currentExists) {
message = tr("Old data from %1 was found, but you already have existing data for %2. Sadly you will need to migrate yourself. Do "
"you want to be reminded of the pending data migration next time you start %2?")
.arg(name, BuildConfig.LAUNCHER_DISPLAYNAME);
} else {
message = tr("It looks like you used %1 before. Do you want to migrate your data to the new location of %2?")
.arg(name, BuildConfig.LAUNCHER_DISPLAYNAME);
QFileInfo logInfo(FS::PathCombine(oldData, name + "-0.log"));
if (logInfo.exists()) {
QString lastModified = logInfo.lastModified().toString(locale.dateFormat());
message = tr("It looks like you used %1 on %2 before. Do you want to migrate your data to the new location of %3?")
.arg(name, lastModified, BuildConfig.LAUNCHER_DISPLAYNAME);
}
}
QMessageBox::StandardButton askMoveDialogue =
QMessageBox::question(nullptr, BuildConfig.LAUNCHER_DISPLAYNAME, message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
auto setDoNotMigrate = [&nomigratePath] {
QFile file(nomigratePath);
file.open(QIODevice::WriteOnly);
};
// create no-migrate file if user doesn't want to migrate
if (askMoveDialogue != QMessageBox::Yes) {
qDebug() << "<> Migration declined for" << name;
setDoNotMigrate();
return currentExists; // cancel further migrations, if we already have a data directory
}
if (!currentExists) {
// Migrate!
auto matcher = std::make_shared<MultiMatcher>();
matcher->add(std::make_shared<SimplePrefixMatcher>(configFile));
matcher->add(std::make_shared<SimplePrefixMatcher>(
BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before
matcher->add(std::make_shared<SimplePrefixMatcher>("logs/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("accounts.json"));
matcher->add(std::make_shared<SimplePrefixMatcher>("accounts/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("assets/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("icons/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("instances/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("libraries/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("mods/"));
matcher->add(std::make_shared<SimplePrefixMatcher>("themes/"));
ProgressDialog diag;
DataMigrationTask task(nullptr, oldData, currentData, matcher);
if (diag.execWithTask(&task)) {
qDebug() << "<> Migration succeeded";
setDoNotMigrate();
} else {
QString reason = task.failReason();
QMessageBox::critical(nullptr, BuildConfig.LAUNCHER_DISPLAYNAME, tr("Migration failed! Reason: %1").arg(reason));
}
} else {
qWarning() << "<> Migration was skipped, due to existing data";
}
return true;
}
void Application::triggerUpdateCheck()
{
if (m_updater) {
qDebug() << "Checking for updates.";
m_updater->setBetaAllowed(false); // There are no other channels than stable
m_updater->checkForUpdates();
} else {
qDebug() << "Updater not available.";
}
}

View File

@ -1,7 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 Tayou <tayou@gmx.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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
@ -42,7 +44,6 @@
#include <QIcon>
#include <QDateTime>
#include <QUrl>
#include <updater/GoUpdate.h>
#include <BaseInstance.h>
@ -62,12 +63,13 @@ class AccountList;
class IconList;
class QNetworkAccessManager;
class JavaInstallList;
class UpdateChecker;
class ExternalUpdater;
class BaseProfilerFactory;
class BaseDetachedToolFactory;
class TranslationsModel;
class ITheme;
class MCEditTool;
class ThemeManager;
namespace Meta {
class Index;
@ -116,18 +118,20 @@ public:
QIcon getThemedIcon(const QString& name);
bool isFlatpak();
void setIconTheme(const QString& name);
std::vector<ITheme *> getValidApplicationThemes();
void applyCurrentlySelectedTheme(bool initial = false);
void setApplicationTheme(const QString& name, bool initial);
QList<ITheme*> getValidApplicationThemes();
shared_qobject_ptr<UpdateChecker> updateChecker() {
return m_updateChecker;
void setApplicationTheme(const QString& name);
shared_qobject_ptr<ExternalUpdater> updater() {
return m_updater;
}
void triggerUpdateCheck();
std::shared_ptr<TranslationsModel> translations();
std::shared_ptr<JavaInstallList> javalist();
@ -174,6 +178,7 @@ public:
QString getMSAClientID();
QString getFlameAPIKey();
QString getModrinthAPIToken();
QString getUserAgent();
QString getUserAgentUncached();
@ -182,6 +187,10 @@ public:
return m_rootPath;
}
bool isPortable() {
return m_portable;
}
const Capabilities capabilities() {
return m_capabilities;
}
@ -200,10 +209,13 @@ public:
void ShowGlobalSettings(class QWidget * parent, QString open_page = QString());
int suitableMaxMem();
signals:
void updateAllowedChanged(bool status);
void globalSettingsAboutToOpen();
void globalSettingsClosed();
int currentCatChanged(int index);
#ifdef Q_OS_MACOS
void clickedOnDock();
@ -229,6 +241,7 @@ private slots:
void setupWizardFinished(int status);
private:
bool handleDataMigration(const QString & currentData, const QString & oldData, const QString & name, const QString & configFile) const;
bool createSetupWizard();
void performMainStartupAction();
@ -245,7 +258,7 @@ private:
shared_qobject_ptr<QNetworkAccessManager> m_network;
shared_qobject_ptr<UpdateChecker> m_updateChecker;
shared_qobject_ptr<ExternalUpdater> m_updater;
shared_qobject_ptr<AccountList> m_accounts;
shared_qobject_ptr<HttpMetaCache> m_metacache;
@ -257,15 +270,16 @@ private:
std::shared_ptr<JavaInstallList> m_javalist;
std::shared_ptr<TranslationsModel> m_translations;
std::shared_ptr<GenericPageProvider> m_globalSettingsProvider;
std::map<QString, std::unique_ptr<ITheme>> m_themes;
std::unique_ptr<MCEditTool> m_mcedit;
QSet<QString> m_features;
std::unique_ptr<ThemeManager> m_themeManager;
QMap<QString, std::shared_ptr<BaseProfilerFactory>> m_profilers;
QString m_rootPath;
Status m_status = Application::StartingUp;
Capabilities m_capabilities;
bool m_portable = false;
#ifdef Q_OS_MACOS
Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive;
@ -300,7 +314,7 @@ public:
QString m_serverToJoin;
QString m_profileToUse;
bool m_liveCheck = false;
QUrl m_zipToImport;
QList<QUrl> m_zipsToImport;
QString m_instanceIdToShowWindowOf;
std::unique_ptr<QFile> logFile;
};

View File

@ -47,8 +47,8 @@ void ApplicationMessage::parse(const QByteArray & input) {
args.clear();
auto parsedArgs = root.value("args").toObject();
for(auto iter = parsedArgs.begin(); iter != parsedArgs.end(); iter++) {
args[iter.key()] = iter.value().toString();
for(auto iter = parsedArgs.constBegin(); iter != parsedArgs.constEnd(); iter++) {
args.insert(iter.key(), iter.value().toString());
}
}
@ -56,8 +56,8 @@ QByteArray ApplicationMessage::serialize() {
QJsonObject root;
root.insert("command", command);
QJsonObject outArgs;
for (auto iter = args.begin(); iter != args.end(); iter++) {
outArgs[iter.key()] = iter.value();
for (auto iter = args.constBegin(); iter != args.constEnd(); iter++) {
outArgs.insert(iter.key(), iter.value());
}
root.insert("args", outArgs);

View File

@ -1,12 +1,12 @@
#pragma once
#include <QString>
#include <QMap>
#include <QHash>
#include <QByteArray>
struct ApplicationMessage {
QString command;
QMap<QString, QString> args;
QHash<QString, QString> args;
QByteArray serialize();
void parse(const QByteArray & input);

View File

@ -17,13 +17,14 @@
#include <memory>
#include "BaseVersion.h"
class MinecraftInstance;
class QDir;
class QString;
class QObject;
class Task;
class BaseVersion;
typedef std::shared_ptr<BaseVersion> BaseVersionPtr;
class BaseInstaller
{
@ -35,7 +36,7 @@ public:
virtual bool add(MinecraftInstance *to);
virtual bool remove(MinecraftInstance *from);
virtual Task *createInstallTask(MinecraftInstance *instance, BaseVersionPtr version, QObject *parent) = 0;
virtual Task *createInstallTask(MinecraftInstance *instance, BaseVersion::Ptr version, QObject *parent) = 0;
protected:
virtual QString id() const = 0;

View File

@ -40,6 +40,8 @@
#include <QDir>
#include <QDebug>
#include <QRegularExpression>
#include <QJsonDocument>
#include <QJsonObject>
#include "settings/INISettingsObject.h"
#include "settings/Setting.h"
@ -64,6 +66,8 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s
m_settings->registerSetting("totalTimePlayed", 0);
m_settings->registerSetting("lastTimePlayed", 0);
m_settings->registerSetting("linkedInstances", "[]");
// Game time override
auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false);
m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride);
@ -182,6 +186,38 @@ bool BaseInstance::shouldStopOnConsoleOverflow() const
return m_settings->get("ConsoleOverflowStop").toBool();
}
QStringList BaseInstance::getLinkedInstances() const
{
return m_settings->get("linkedInstances").toStringList();
}
void BaseInstance::setLinkedInstances(const QStringList& list)
{
auto linkedInstances = m_settings->get("linkedInstances").toStringList();
m_settings->set("linkedInstances", list);
}
void BaseInstance::addLinkedInstanceId(const QString& id)
{
auto linkedInstances = m_settings->get("linkedInstances").toStringList();
linkedInstances.append(id);
setLinkedInstances(linkedInstances);
}
bool BaseInstance::removeLinkedInstanceId(const QString& id)
{
auto linkedInstances = m_settings->get("linkedInstances").toStringList();
int numRemoved = linkedInstances.removeAll(id);
setLinkedInstances(linkedInstances);
return numRemoved > 0;
}
bool BaseInstance::isLinkedToInstanceId(const QString& id) const
{
auto linkedInstances = m_settings->get("linkedInstances").toStringList();
return linkedInstances.contains(id);
}
void BaseInstance::iconUpdated(QString key)
{
if(iconKey() == key)

View File

@ -151,7 +151,7 @@ public:
void copyManagedPack(BaseInstance& other);
/// guess log level from a line of game log
virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level)
virtual MessageLevel::Enum guessLevel([[maybe_unused]] const QString &line, MessageLevel::Enum level)
{
return level;
};
@ -282,6 +282,12 @@ public:
int getConsoleMaxLines() const;
bool shouldStopOnConsoleOverflow() const;
QStringList getLinkedInstances() const;
void setLinkedInstances(const QStringList& list);
void addLinkedInstanceId(const QString& id);
bool removeLinkedInstanceId(const QString& id);
bool isLinkedToInstanceId(const QString& id) const;
protected:
void changeStatus(Status newStatus);

View File

@ -25,6 +25,7 @@
class BaseVersion
{
public:
using Ptr = std::shared_ptr<BaseVersion>;
virtual ~BaseVersion() {}
/*!
* A string used to identify this version in config files.
@ -54,6 +55,4 @@ public:
};
};
typedef std::shared_ptr<BaseVersion> BaseVersionPtr;
Q_DECLARE_METATYPE(BaseVersionPtr)
Q_DECLARE_METATYPE(BaseVersion::Ptr)

View File

@ -40,20 +40,20 @@ BaseVersionList::BaseVersionList(QObject *parent) : QAbstractListModel(parent)
{
}
BaseVersionPtr BaseVersionList::findVersion(const QString &descriptor)
BaseVersion::Ptr BaseVersionList::findVersion(const QString &descriptor)
{
for (int i = 0; i < count(); i++)
{
if (at(i)->descriptor() == descriptor)
return at(i);
}
return BaseVersionPtr();
return nullptr;
}
BaseVersionPtr BaseVersionList::getRecommended() const
BaseVersion::Ptr BaseVersionList::getRecommended() const
{
if (count() <= 0)
return BaseVersionPtr();
return nullptr;
else
return at(0);
}
@ -66,7 +66,7 @@ QVariant BaseVersionList::data(const QModelIndex &index, int role) const
if (index.row() > count())
return QVariant();
BaseVersionPtr version = at(index.row());
BaseVersion::Ptr version = at(index.row());
switch (role)
{
@ -95,12 +95,12 @@ BaseVersionList::RoleList BaseVersionList::providesRoles() const
int BaseVersionList::rowCount(const QModelIndex &parent) const
{
// Return count
return count();
return parent.isValid() ? 0 : count();
}
int BaseVersionList::columnCount(const QModelIndex &parent) const
{
return 1;
return parent.isValid() ? 0 : 1;
}
QHash<int, QByteArray> BaseVersionList::roleNames() const

View File

@ -70,7 +70,7 @@ public:
virtual bool isLoaded() = 0;
//! Gets the version at the given index.
virtual const BaseVersionPtr at(int i) const = 0;
virtual const BaseVersion::Ptr at(int i) const = 0;
//! Returns the number of versions in the list.
virtual int count() const = 0;
@ -90,13 +90,13 @@ public:
* \return A const pointer to the version with the given descriptor. NULL if
* one doesn't exist.
*/
virtual BaseVersionPtr findVersion(const QString &descriptor);
virtual BaseVersion::Ptr findVersion(const QString &descriptor);
/*!
* \brief Gets the recommended version from this list
* If the list doesn't support recommended versions, this works exactly as getLatestStable
*/
virtual BaseVersionPtr getRecommended() const;
virtual BaseVersion::Ptr getRecommended() const;
/*!
* Sorts the version list.
@ -117,5 +117,5 @@ slots:
* then copies the versions and sets their parents correctly.
* \param versions List of versions whose parents should be set.
*/
virtual void updateListData(QList<BaseVersionPtr> versions) = 0;
virtual void updateListData(QList<BaseVersion::Ptr> versions) = 0;
};

View File

@ -24,21 +24,24 @@ set(CORE_SOURCES
NullInstance.h
MMCZip.h
MMCZip.cpp
MMCStrings.h
MMCStrings.cpp
StringUtils.h
StringUtils.cpp
QVariantUtils.h
RuntimeContext.h
# Basic instance manipulation tasks (derived from InstanceTask)
InstanceCreationTask.h
InstanceCreationTask.cpp
InstanceCopyPrefs.h
InstanceCopyPrefs.cpp
InstanceCopyTask.h
InstanceCopyTask.cpp
InstanceImportTask.h
InstanceImportTask.cpp
# Mod downloading task
ModDownloadTask.h
ModDownloadTask.cpp
# Resource downloading task
ResourceDownloadTask.h
ResourceDownloadTask.cpp
# Use tracking separate from memory management
Usable.h
@ -87,7 +90,18 @@ set(CORE_SOURCES
# Time
MMCTime.h
MMCTime.cpp
MTPixmapCache.h
)
if (UNIX AND NOT CYGWIN AND NOT APPLE)
set(CORE_SOURCES
${CORE_SOURCES}
# MangoHud
MangoHud.h
MangoHud.cpp
)
endif()
set(PATHMATCHER_SOURCES
# Path matchers
@ -95,6 +109,7 @@ set(PATHMATCHER_SOURCES
pathmatcher/IPathMatcher.h
pathmatcher/MultiMatcher.h
pathmatcher/RegexpMatcher.h
pathmatcher/SimplePrefixMatcher.h
)
set(NET_SOURCES
@ -109,6 +124,8 @@ set(NET_SOURCES
net/HttpMetaCache.h
net/MetaCacheSink.cpp
net/MetaCacheSink.h
net/Logging.h
net/Logging.cpp
net/NetAction.h
net/NetJob.cpp
net/NetJob.h
@ -147,12 +164,6 @@ set(LAUNCH_SOURCES
# Old update system
set(UPDATE_SOURCES
updater/GoUpdate.h
updater/GoUpdate.cpp
updater/UpdateChecker.h
updater/UpdateChecker.cpp
updater/DownloadTask.h
updater/DownloadTask.cpp
updater/ExternalUpdater.h
)
@ -317,12 +328,18 @@ set(MINECRAFT_SOURCES
minecraft/mod/Resource.cpp
minecraft/mod/ResourceFolderModel.h
minecraft/mod/ResourceFolderModel.cpp
minecraft/mod/DataPack.h
minecraft/mod/DataPack.cpp
minecraft/mod/ResourcePack.h
minecraft/mod/ResourcePack.cpp
minecraft/mod/ResourcePackFolderModel.h
minecraft/mod/ResourcePackFolderModel.cpp
minecraft/mod/TexturePack.h
minecraft/mod/TexturePack.cpp
minecraft/mod/ShaderPack.h
minecraft/mod/ShaderPack.cpp
minecraft/mod/WorldSave.h
minecraft/mod/WorldSave.cpp
minecraft/mod/TexturePackFolderModel.h
minecraft/mod/TexturePackFolderModel.cpp
minecraft/mod/ShaderPackFolderModel.h
@ -333,10 +350,20 @@ set(MINECRAFT_SOURCES
minecraft/mod/tasks/LocalModParseTask.cpp
minecraft/mod/tasks/LocalModUpdateTask.h
minecraft/mod/tasks/LocalModUpdateTask.cpp
minecraft/mod/tasks/LocalDataPackParseTask.h
minecraft/mod/tasks/LocalDataPackParseTask.cpp
minecraft/mod/tasks/LocalResourcePackParseTask.h
minecraft/mod/tasks/LocalResourcePackParseTask.cpp
minecraft/mod/tasks/LocalTexturePackParseTask.h
minecraft/mod/tasks/LocalTexturePackParseTask.cpp
minecraft/mod/tasks/LocalShaderPackParseTask.h
minecraft/mod/tasks/LocalShaderPackParseTask.cpp
minecraft/mod/tasks/LocalWorldSaveParseTask.h
minecraft/mod/tasks/LocalWorldSaveParseTask.cpp
minecraft/mod/tasks/LocalResourceParse.h
minecraft/mod/tasks/LocalResourceParse.cpp
minecraft/mod/tasks/GetModDependenciesTask.h
minecraft/mod/tasks/GetModDependenciesTask.cpp
# Assets
minecraft/AssetsUtils.h
@ -350,8 +377,6 @@ set(MINECRAFT_SOURCES
minecraft/services/SkinDelete.cpp
minecraft/services/SkinDelete.h
mojang/PackageManifest.h
mojang/PackageManifest.cpp
minecraft/Agent.h)
# the screenshots feature
@ -445,7 +470,7 @@ set(API_SOURCES
modplatform/ModIndex.h
modplatform/ModIndex.cpp
modplatform/ModAPI.h
modplatform/ResourceAPI.h
modplatform/EnsureMetadataTask.h
modplatform/EnsureMetadataTask.cpp
@ -456,12 +481,15 @@ set(API_SOURCES
modplatform/flame/FlameAPI.cpp
modplatform/modrinth/ModrinthAPI.h
modplatform/modrinth/ModrinthAPI.cpp
modplatform/helpers/NetworkModAPI.h
modplatform/helpers/NetworkModAPI.cpp
modplatform/helpers/NetworkResourceAPI.h
modplatform/helpers/NetworkResourceAPI.cpp
modplatform/helpers/HashUtils.h
modplatform/helpers/HashUtils.cpp
modplatform/helpers/OverrideUtils.h
modplatform/helpers/OverrideUtils.cpp
modplatform/helpers/ExportToModList.h
modplatform/helpers/ExportToModList.cpp
)
set(FTB_SOURCES
@ -489,6 +517,8 @@ set(FLAME_SOURCES
modplatform/flame/FlameCheckUpdate.h
modplatform/flame/FlameInstanceCreationTask.h
modplatform/flame/FlameInstanceCreationTask.cpp
modplatform/flame/FlamePackExportTask.h
modplatform/flame/FlamePackExportTask.cpp
)
set(MODRINTH_SOURCES
@ -500,13 +530,8 @@ set(MODRINTH_SOURCES
modplatform/modrinth/ModrinthCheckUpdate.h
modplatform/modrinth/ModrinthInstanceCreationTask.cpp
modplatform/modrinth/ModrinthInstanceCreationTask.h
)
set(MODPACKSCH_SOURCES
modplatform/modpacksch/FTBPackInstallTask.h
modplatform/modpacksch/FTBPackInstallTask.cpp
modplatform/modpacksch/FTBPackManifest.h
modplatform/modpacksch/FTBPackManifest.cpp
modplatform/modrinth/ModrinthPackExportTask.cpp
modplatform/modrinth/ModrinthPackExportTask.h
)
set(PACKWIZ_SOURCES
@ -537,10 +562,86 @@ set(ATLAUNCHER_SOURCES
modplatform/atlauncher/ATLShareCode.h
)
################################ COMPILE ################################
set(LINKEXE_SOURCES
filelink/FileLink.h
filelink/FileLink.cpp
FileSystem.h
FileSystem.cpp
Exception.h
StringUtils.h
StringUtils.cpp
DesktopServices.h
DesktopServices.cpp
)
# we need zlib
find_package(ZLIB REQUIRED)
######## Logging categories ########
ecm_qt_declare_logging_category(CORE_SOURCES
HEADER Logging.h
IDENTIFIER authCredentials
CATEGORY_NAME "launcher.auth.credentials"
DEFAULT_SEVERITY Warning
DESCRIPTION "Secrets and credentials for debugging purposes"
EXPORT "${Launcher_Name}"
)
ecm_qt_export_logging_category(
IDENTIFIER taskLogC
CATEGORY_NAME "launcher.task"
DEFAULT_SEVERITY Debug
DESCRIPTION "Task actions"
EXPORT "${Launcher_Name}"
)
ecm_qt_export_logging_category(
IDENTIFIER taskNetLogC
CATEGORY_NAME "launcher.task.net"
DEFAULT_SEVERITY Debug
DESCRIPTION "task network action"
EXPORT "${Launcher_Name}"
)
ecm_qt_export_logging_category(
IDENTIFIER taskDownloadLogC
CATEGORY_NAME "launcher.task.net.download"
DEFAULT_SEVERITY Debug
DESCRIPTION "task network download actions"
EXPORT "${Launcher_Name}"
)
ecm_qt_export_logging_category(
IDENTIFIER taskUploadLogC
CATEGORY_NAME "launcher.task.net.upload"
DEFAULT_SEVERITY Debug
DESCRIPTION "task network upload actions"
EXPORT "${Launcher_Name}"
)
ecm_qt_export_logging_category(
IDENTIFIER taskMetaCacheLogC
CATEGORY_NAME "launcher.task.net.metacache"
DEFAULT_SEVERITY Debug
DESCRIPTION "task network meta-cache actions"
EXPORT "${Launcher_Name}"
)
ecm_qt_export_logging_category(
IDENTIFIER taskHttpMetaCacheLogC
CATEGORY_NAME "launcher.task.net.metacache.http"
DEFAULT_SEVERITY Debug
DESCRIPTION "task network http meta-cache actions"
EXPORT "${Launcher_Name}"
)
if(KDE_INSTALL_LOGGINGCATEGORIESDIR) # only install if there is a standard path for this
ecm_qt_install_logging_categories(
EXPORT "${Launcher_Name}"
DESTINATION "${KDE_INSTALL_LOGGINGCATEGORIESDIR}"
)
endif()
################################ COMPILE ################################
set(LOGIC_SOURCES
${CORE_SOURCES}
@ -562,7 +663,6 @@ set(LOGIC_SOURCES
${FTB_SOURCES}
${FLAME_SOURCES}
${MODRINTH_SOURCES}
${MODPACKSCH_SOURCES}
${PACKWIZ_SOURCES}
${TECHNIC_SOURCES}
${ATLAUNCHER_SOURCES}
@ -576,8 +676,8 @@ SET(LAUNCHER_SOURCES
# Application base
Application.h
Application.cpp
UpdateController.cpp
UpdateController.h
DataMigrationTask.h
DataMigrationTask.cpp
ApplicationMessage.h
ApplicationMessage.cpp
SysInfo.h
@ -588,7 +688,8 @@ SET(LAUNCHER_SOURCES
DesktopServices.cpp
VersionProxyModel.h
VersionProxyModel.cpp
HoeDown.h
Markdown.h
Markdown.cpp
# Super secret!
KonamiCode.h
@ -601,9 +702,12 @@ SET(LAUNCHER_SOURCES
resources/pe_light/pe_light.qrc
resources/pe_colored/pe_colored.qrc
resources/pe_blue/pe_blue.qrc
resources/breeze_dark/breeze_dark.qrc
resources/breeze_light/breeze_light.qrc
resources/OSX/OSX.qrc
resources/iOS/iOS.qrc
resources/flat/flat.qrc
resources/flat_white/flat_white.qrc
resources/documents/documents.qrc
../${Launcher_Branding_LogoQRC}
@ -626,6 +730,10 @@ SET(LAUNCHER_SOURCES
# FIXME: maybe find a better home for this.
SkinUtils.cpp
SkinUtils.h
FileIgnoreProxy.cpp
FileIgnoreProxy.h
FastFileIconProvider.cpp
FastFileIconProvider.h
# GUI - setup wizard
ui/setupwizard/SetupWizard.h
@ -637,6 +745,8 @@ SET(LAUNCHER_SOURCES
ui/setupwizard/LanguageWizardPage.h
ui/setupwizard/PasteWizardPage.cpp
ui/setupwizard/PasteWizardPage.h
ui/setupwizard/ThemeWizardPage.cpp
ui/setupwizard/ThemeWizardPage.h
# GUI - themes
ui/themes/FusionTheme.cpp
@ -651,6 +761,8 @@ SET(LAUNCHER_SOURCES
ui/themes/ITheme.h
ui/themes/SystemTheme.cpp
ui/themes/SystemTheme.h
ui/themes/ThemeManager.cpp
ui/themes/ThemeManager.h
# Processes
LaunchController.h
@ -675,9 +787,14 @@ SET(LAUNCHER_SOURCES
ui/pages/instance/GameOptionsPage.h
ui/pages/instance/VersionPage.cpp
ui/pages/instance/VersionPage.h
ui/pages/instance/ManagedPackPage.cpp
ui/pages/instance/ManagedPackPage.h
ui/pages/instance/TexturePackPage.h
ui/pages/instance/TexturePackPage.cpp
ui/pages/instance/ResourcePackPage.h
ui/pages/instance/ResourcePackPage.cpp
ui/pages/instance/ShaderPackPage.h
ui/pages/instance/ShaderPackPage.cpp
ui/pages/instance/ModFolderPage.cpp
ui/pages/instance/ModFolderPage.h
ui/pages/instance/NotesPage.cpp
@ -716,14 +833,29 @@ SET(LAUNCHER_SOURCES
ui/pages/global/APIPage.h
# GUI - platform pages
ui/pages/modplatform/VanillaPage.cpp
ui/pages/modplatform/VanillaPage.h
ui/pages/modplatform/CustomPage.cpp
ui/pages/modplatform/CustomPage.h
ui/pages/modplatform/ResourcePage.cpp
ui/pages/modplatform/ResourcePage.h
ui/pages/modplatform/ResourceModel.cpp
ui/pages/modplatform/ResourceModel.h
ui/pages/modplatform/ModPage.cpp
ui/pages/modplatform/ModPage.h
ui/pages/modplatform/ModModel.cpp
ui/pages/modplatform/ModModel.h
ui/pages/modplatform/ResourcePackPage.cpp
ui/pages/modplatform/ResourcePackModel.cpp
# Needed for MOC to find them without a corresponding .cpp
ui/pages/modplatform/TexturePackPage.h
ui/pages/modplatform/TexturePackModel.cpp
ui/pages/modplatform/ShaderPackPage.cpp
ui/pages/modplatform/ShaderPackModel.cpp
ui/pages/modplatform/atlauncher/AtlFilterModel.cpp
ui/pages/modplatform/atlauncher/AtlFilterModel.h
ui/pages/modplatform/atlauncher/AtlListModel.cpp
@ -735,13 +867,6 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp
ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h
ui/pages/modplatform/ftb/FtbFilterModel.cpp
ui/pages/modplatform/ftb/FtbFilterModel.h
ui/pages/modplatform/ftb/FtbListModel.cpp
ui/pages/modplatform/ftb/FtbListModel.h
ui/pages/modplatform/ftb/FtbPage.cpp
ui/pages/modplatform/ftb/FtbPage.h
ui/pages/modplatform/legacy_ftb/Page.cpp
ui/pages/modplatform/legacy_ftb/Page.h
ui/pages/modplatform/legacy_ftb/ListModel.h
@ -751,10 +876,10 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/flame/FlameModel.h
ui/pages/modplatform/flame/FlamePage.cpp
ui/pages/modplatform/flame/FlamePage.h
ui/pages/modplatform/flame/FlameModModel.cpp
ui/pages/modplatform/flame/FlameModModel.h
ui/pages/modplatform/flame/FlameModPage.cpp
ui/pages/modplatform/flame/FlameModPage.h
ui/pages/modplatform/flame/FlameResourceModels.cpp
ui/pages/modplatform/flame/FlameResourceModels.h
ui/pages/modplatform/flame/FlameResourcePages.cpp
ui/pages/modplatform/flame/FlameResourcePages.h
ui/pages/modplatform/modrinth/ModrinthPage.cpp
ui/pages/modplatform/modrinth/ModrinthPage.h
@ -769,10 +894,10 @@ SET(LAUNCHER_SOURCES
ui/pages/modplatform/ImportPage.cpp
ui/pages/modplatform/ImportPage.h
ui/pages/modplatform/modrinth/ModrinthModModel.cpp
ui/pages/modplatform/modrinth/ModrinthModModel.h
ui/pages/modplatform/modrinth/ModrinthModPage.cpp
ui/pages/modplatform/modrinth/ModrinthModPage.h
ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp
ui/pages/modplatform/modrinth/ModrinthResourceModels.h
ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp
ui/pages/modplatform/modrinth/ModrinthResourcePages.h
# GUI - dialogs
ui/dialogs/AboutDialog.cpp
@ -789,8 +914,14 @@ SET(LAUNCHER_SOURCES
ui/dialogs/EditAccountDialog.h
ui/dialogs/ExportInstanceDialog.cpp
ui/dialogs/ExportInstanceDialog.h
ui/dialogs/ExportPackDialog.cpp
ui/dialogs/ExportPackDialog.h
ui/dialogs/ExportToModListDialog.cpp
ui/dialogs/ExportToModListDialog.h
ui/dialogs/IconPickerDialog.cpp
ui/dialogs/IconPickerDialog.h
ui/dialogs/ImportResourceDialog.cpp
ui/dialogs/ImportResourceDialog.h
ui/dialogs/LoginDialog.cpp
ui/dialogs/LoginDialog.h
ui/dialogs/MSALoginDialog.cpp
@ -809,14 +940,12 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ProgressDialog.h
ui/dialogs/ReviewMessageBox.cpp
ui/dialogs/ReviewMessageBox.h
ui/dialogs/UpdateDialog.cpp
ui/dialogs/UpdateDialog.h
ui/dialogs/VersionSelectDialog.cpp
ui/dialogs/VersionSelectDialog.h
ui/dialogs/SkinUploadDialog.cpp
ui/dialogs/SkinUploadDialog.h
ui/dialogs/ModDownloadDialog.cpp
ui/dialogs/ModDownloadDialog.h
ui/dialogs/ResourceDownloadDialog.cpp
ui/dialogs/ResourceDownloadDialog.h
ui/dialogs/ScrollMessageBox.cpp
ui/dialogs/ScrollMessageBox.h
ui/dialogs/BlockedModsDialog.cpp
@ -862,6 +991,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/VariableSizedImageObject.cpp
ui/widgets/ProjectItem.h
ui/widgets/ProjectItem.cpp
ui/widgets/SubTaskProgressBar.h
ui/widgets/SubTaskProgressBar.cpp
ui/widgets/VersionListView.cpp
ui/widgets/VersionListView.h
ui/widgets/VersionSelectWidget.cpp
@ -870,6 +1001,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/ProgressWidget.cpp
ui/widgets/WideBar.h
ui/widgets/WideBar.cpp
ui/widgets/ThemeCustomizationWidget.h
ui/widgets/ThemeCustomizationWidget.cpp
# GUI - instance group view
ui/instanceview/InstanceProxyModel.cpp
@ -887,18 +1020,10 @@ SET(LAUNCHER_SOURCES
JavaDownloader.h
)
if(WIN32)
set(LAUNCHER_SOURCES
${LAUNCHER_SOURCES}
# GUI - dark titlebar for Windows 10/11
ui/WinDarkmode.h
ui/WinDarkmode.cpp
)
endif()
qt_wrap_ui(LAUNCHER_UI
ui/MainWindow.ui
ui/setupwizard/PasteWizardPage.ui
ui/setupwizard/ThemeWizardPage.ui
ui/pages/global/AccountListPage.ui
ui/pages/global/JavaPage.ui
ui/pages/global/LauncherPage.ui
@ -914,33 +1039,37 @@ qt_wrap_ui(LAUNCHER_UI
ui/pages/instance/OtherLogsPage.ui
ui/pages/instance/InstanceSettingsPage.ui
ui/pages/instance/VersionPage.ui
ui/pages/instance/ManagedPackPage.ui
ui/pages/instance/WorldListPage.ui
ui/pages/instance/ScreenshotsPage.ui
ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
ui/pages/modplatform/atlauncher/AtlPage.ui
ui/pages/modplatform/VanillaPage.ui
ui/pages/modplatform/ModPage.ui
ui/pages/modplatform/CustomPage.ui
ui/pages/modplatform/ResourcePage.ui
ui/pages/modplatform/flame/FlamePage.ui
ui/pages/modplatform/legacy_ftb/Page.ui
ui/pages/modplatform/ImportPage.ui
ui/pages/modplatform/ftb/FtbPage.ui
ui/pages/modplatform/modrinth/ModrinthPage.ui
ui/pages/modplatform/technic/TechnicPage.ui
ui/widgets/InstanceCardWidget.ui
ui/widgets/CustomCommands.ui
ui/widgets/InfoFrame.ui
ui/widgets/ModFilterWidget.ui
ui/widgets/SubTaskProgressBar.ui
ui/widgets/ThemeCustomizationWidget.ui
ui/dialogs/CopyInstanceDialog.ui
ui/dialogs/ProfileSetupDialog.ui
ui/dialogs/ProgressDialog.ui
ui/dialogs/NewInstanceDialog.ui
ui/dialogs/UpdateDialog.ui
ui/dialogs/NewComponentDialog.ui
ui/dialogs/NewsDialog.ui
ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/SkinUploadDialog.ui
ui/dialogs/ExportInstanceDialog.ui
ui/dialogs/ExportPackDialog.ui
ui/dialogs/ExportToModListDialog.ui
ui/dialogs/IconPickerDialog.ui
ui/dialogs/ImportResourceDialog.ui
ui/dialogs/MSALoginDialog.ui
ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/AboutDialog.ui
@ -959,6 +1088,8 @@ qt_add_resources(LAUNCHER_RESOURCES
resources/pe_light/pe_light.qrc
resources/pe_colored/pe_colored.qrc
resources/pe_blue/pe_blue.qrc
resources/breeze_dark/breeze_dark.qrc
resources/breeze_light/breeze_light.qrc
resources/OSX/OSX.qrc
resources/iOS/iOS.qrc
resources/flat/flat.qrc
@ -980,6 +1111,7 @@ target_link_libraries(Launcher_logic
nbt++
${ZLIB_LIBRARIES}
tomlplusplus::tomlplusplus
qdcss
BuildConfig
Katabasis
Qt${QT_VERSION_MAJOR}::Widgets
@ -1003,7 +1135,7 @@ target_link_libraries(Launcher_logic
)
target_link_libraries(Launcher_logic
QuaZip::QuaZip
hoedown
cmark::cmark
LocalPeer
Launcher_rainbow
)
@ -1048,6 +1180,41 @@ install(TARGETS ${Launcher_Name}
FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime
)
if(WIN32)
add_library(filelink_logic STATIC ${LINKEXE_SOURCES})
target_include_directories(filelink_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(filelink_logic
systeminfo
BuildConfig
ghcFilesystem::ghc_filesystem
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network
# Qt${QT_VERSION_MAJOR}::Concurrent
${Launcher_QT_LIBS}
)
add_executable("${Launcher_Name}_filelink" WIN32 filelink/main.cpp)
target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest)
target_link_libraries("${Launcher_Name}_filelink" filelink_logic)
if(DEFINED Launcher_APP_BINARY_NAME)
set_target_properties("${Launcher_Name}_filelink" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_filelink")
endif()
if(DEFINED Launcher_BINARY_RPATH)
SET_TARGET_PROPERTIES("${Launcher_Name}_filelink" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}")
endif()
install(TARGETS "${Launcher_Name}_filelink"
BUNDLE DESTINATION "." COMPONENT Runtime
LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime
RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime
FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime
)
endif()
if (UNIX AND APPLE)
# Add Sparkle updater
# It has to be copied here instead of just allowing fixup_bundle to install it, otherwise essential parts of
@ -1064,97 +1231,106 @@ if(INSTALL_BUNDLE STREQUAL "full")
CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")"
COMPONENT Runtime
)
# add qtlogging.ini as a config file
install(
FILES "qtlogging.ini"
DESTINATION ${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}
COMPONENT Runtime
)
# Bundle plugins
if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
# Image formats
# Image formats
install(
DIRECTORY "${QT_PLUGINS_DIR}/imageformats"
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "tga|tiff|mng" EXCLUDE
)
install(
DIRECTORY "${QT_PLUGINS_DIR}/imageformats"
CONFIGURATIONS Release MinSizeRel
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "tga|tiff|mng" EXCLUDE
REGEX "d\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
# Icon engines
install(
DIRECTORY "${QT_PLUGINS_DIR}/iconengines"
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "fontawesome" EXCLUDE
)
install(
DIRECTORY "${QT_PLUGINS_DIR}/iconengines"
CONFIGURATIONS Release MinSizeRel
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "fontawesome" EXCLUDE
REGEX "d\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
# Platform plugins
install(
DIRECTORY "${QT_PLUGINS_DIR}/platforms"
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "minimal|linuxfb|offscreen" EXCLUDE
)
install(
DIRECTORY "${QT_PLUGINS_DIR}/platforms"
CONFIGURATIONS Release MinSizeRel
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "minimal|linuxfb|offscreen" EXCLUDE
REGEX "[^2]d\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
# Style plugins
if(EXISTS "${QT_PLUGINS_DIR}/styles")
install(
DIRECTORY "${QT_PLUGINS_DIR}/imageformats"
DIRECTORY "${QT_PLUGINS_DIR}/styles"
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "tga|tiff|mng" EXCLUDE
)
# Icon engines
install(
DIRECTORY "${QT_PLUGINS_DIR}/iconengines"
DIRECTORY "${QT_PLUGINS_DIR}/styles"
CONFIGURATIONS Release MinSizeRel
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "fontawesome" EXCLUDE
)
# Platform plugins
install(
DIRECTORY "${QT_PLUGINS_DIR}/platforms"
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "minimal|linuxfb|offscreen" EXCLUDE
)
# Style plugins
if(EXISTS "${QT_PLUGINS_DIR}/styles")
install(
DIRECTORY "${QT_PLUGINS_DIR}/styles"
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
)
endif()
# TLS plugins (Qt 6 only)
if(EXISTS "${QT_PLUGINS_DIR}/tls")
install(
DIRECTORY "${QT_PLUGINS_DIR}/tls"
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
)
endif()
else()
# Image formats
install(
DIRECTORY "${QT_PLUGINS_DIR}/imageformats"
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "tga|tiff|mng" EXCLUDE
REGEX "d\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
# Icon engines
endif()
# TLS plugins (Qt 6 only)
if(EXISTS "${QT_PLUGINS_DIR}/tls")
install(
DIRECTORY "${QT_PLUGINS_DIR}/iconengines"
DIRECTORY "${QT_PLUGINS_DIR}/tls"
CONFIGURATIONS Debug RelWithDebInfo ""
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "fontawesome" EXCLUDE
REGEX "d\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
PATTERN "*qopensslbackend*" EXCLUDE
PATTERN "*qcertonlybackend*" EXCLUDE
)
# Platform plugins
install(
DIRECTORY "${QT_PLUGINS_DIR}/platforms"
DIRECTORY "${QT_PLUGINS_DIR}/tls"
CONFIGURATIONS Release MinSizeRel
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "minimal|linuxfb|offscreen" EXCLUDE
REGEX "d\\." EXCLUDE
REGEX "dd\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
PATTERN "*qopensslbackend*" EXCLUDE
PATTERN "*qcertonlybackend*" EXCLUDE
)
# Style plugins
if(EXISTS "${QT_PLUGINS_DIR}/styles")
install(
DIRECTORY "${QT_PLUGINS_DIR}/styles"
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "d\\." EXCLUDE
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
endif()
# TLS plugins (Qt 6 only)
if(EXISTS "${QT_PLUGINS_DIR}/tls")
install(
DIRECTORY "${QT_PLUGINS_DIR}/tls"
DESTINATION ${PLUGIN_DEST_DIR}
COMPONENT Runtime
REGEX "_debug\\." EXCLUDE
REGEX "\\.dSYM" EXCLUDE
)
endif()
endif()
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in"

View File

@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "DataMigrationTask.h"
#include "FileSystem.h"
#include <QDirIterator>
#include <QFileInfo>
#include <QMap>
#include <QtConcurrent>
DataMigrationTask::DataMigrationTask(QObject* parent,
const QString& sourcePath,
const QString& targetPath,
const IPathMatcher::Ptr pathMatcher)
: Task(parent), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath)
{
m_copy.matcher(m_pathMatcher.get()).whitelist(true);
}
void DataMigrationTask::executeTask()
{
setStatus(tr("Scanning files..."));
// 1. Scan
// Check how many files we gotta copy
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] {
return m_copy(true); // dry run to collect amount of files
});
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::dryRunFinished);
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::dryRunAborted);
m_copyFutureWatcher.setFuture(m_copyFuture);
}
void DataMigrationTask::dryRunFinished()
{
disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::dryRunFinished);
disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::dryRunAborted);
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
if (!m_copyFuture.isValid() || !m_copyFuture.result()) {
#else
if (!m_copyFuture.result()) {
#endif
emitFailed(tr("Failed to scan source path."));
return;
}
// 2. Copy
// Actually copy all files now.
m_toCopy = m_copy.totalCopied();
connect(&m_copy, &FS::copy::fileCopied, [&, this](const QString& relativeName) {
QString shortenedName = relativeName;
// shorten the filename to hopefully fit into one line
if (shortenedName.length() > 50)
shortenedName = relativeName.left(20) + "" + relativeName.right(29);
setProgress(m_copy.totalCopied(), m_toCopy);
setStatus(tr("Copying %1…").arg(shortenedName));
});
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] {
return m_copy(false); // actually copy now
});
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::copyFinished);
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::copyAborted);
m_copyFutureWatcher.setFuture(m_copyFuture);
}
void DataMigrationTask::dryRunAborted()
{
emitFailed(tr("Aborted"));
}
void DataMigrationTask::copyFinished()
{
disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &DataMigrationTask::copyFinished);
disconnect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &DataMigrationTask::copyAborted);
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
if (!m_copyFuture.isValid() || !m_copyFuture.result()) {
#else
if (!m_copyFuture.result()) {
#endif
emitFailed(tr("Some paths could not be copied!"));
return;
}
emitSucceeded();
}
void DataMigrationTask::copyAborted()
{
emitFailed(tr("Aborted"));
}

View File

@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu <contact@scrumplex.net>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "FileSystem.h"
#include "pathmatcher/IPathMatcher.h"
#include "tasks/Task.h"
#include <QFuture>
#include <QFutureWatcher>
/*
* Migrate existing data from other MMC-like launchers.
*/
class DataMigrationTask : public Task {
Q_OBJECT
public:
explicit DataMigrationTask(QObject* parent, const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathmatcher);
~DataMigrationTask() override = default;
protected:
virtual void executeTask() override;
protected slots:
void dryRunFinished();
void dryRunAborted();
void copyFinished();
void copyAborted();
private:
const QString& m_sourcePath;
const QString& m_targetPath;
const IPathMatcher::Ptr m_pathMatcher;
FS::copy m_copy;
int m_toCopy = 0;
QFuture<bool> m_copyFuture;
QFutureWatcher<bool> m_copyFutureWatcher;
};

View File

@ -37,7 +37,6 @@
#include <QDesktopServices>
#include <QProcess>
#include <QDebug>
#include "Application.h"
/**
* This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing.
@ -119,7 +118,7 @@ bool openDirectory(const QString &path, bool ensureExists)
return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath()));
};
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if(!APPLICATION->isFlatpak())
if(!isFlatpak())
{
return IndirectOpen(f);
}
@ -140,7 +139,7 @@ bool openFile(const QString &path)
return QDesktopServices::openUrl(QUrl::fromLocalFile(path));
};
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if(!APPLICATION->isFlatpak())
if(!isFlatpak())
{
return IndirectOpen(f);
}
@ -158,7 +157,7 @@ bool openFile(const QString &application, const QString &path, const QString &wo
qDebug() << "Opening file" << path << "using" << application;
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
// FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
if(!APPLICATION->isFlatpak())
if(!isFlatpak())
{
return IndirectOpen([&]()
{
@ -178,7 +177,7 @@ bool run(const QString &application, const QStringList &args, const QString &wor
{
qDebug() << "Running" << application << "with args" << args.join(' ');
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if(!APPLICATION->isFlatpak())
if(!isFlatpak())
{
// FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
return IndirectOpen([&]()
@ -203,7 +202,7 @@ bool openUrl(const QUrl &url)
return QDesktopServices::openUrl(url);
};
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
if(!APPLICATION->isFlatpak())
if(!isFlatpak())
{
return IndirectOpen(f);
}
@ -216,4 +215,13 @@ bool openUrl(const QUrl &url)
#endif
}
bool isFlatpak()
{
#ifdef Q_OS_LINUX
return QFile::exists("/.flatpak-info");
#else
return false;
#endif
}
}

View File

@ -33,4 +33,6 @@ namespace DesktopServices
* Open the URL, most likely in a browser. Maybe.
*/
bool openUrl(const QUrl &url);
bool isFlatpak();
}

View File

@ -0,0 +1,47 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "FastFileIconProvider.h"
#include <QApplication>
#include <QStyle>
QIcon FastFileIconProvider::icon(const QFileInfo& info) const
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
bool link = info.isSymbolicLink() || info.isAlias() || info.isShortcut();
#else
// in versions prior to 6.4 we don't have access to isAlias
bool link = info.isSymLink();
#endif
QStyle::StandardPixmap icon;
if (info.isDir()) {
if (link)
icon = QStyle::SP_DirLinkIcon;
else
icon = QStyle::SP_DirIcon;
} else {
if (link)
icon = QStyle::SP_FileLinkIcon;
else
icon = QStyle::SP_FileIcon;
}
return QApplication::style()->standardIcon(icon);
}

View File

@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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/>.
*/
#pragma once
#include <QFileIconProvider>
class FastFileIconProvider : public QFileIconProvider {
public:
QIcon icon(const QFileInfo& info) const override;
};

View File

@ -0,0 +1,279 @@
// 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>
*
* 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 "FileIgnoreProxy.h"
#include <QDebug>
#include <QFileSystemModel>
#include <QSortFilterProxyModel>
#include <QStack>
#include <algorithm>
#include "FileSystem.h"
#include "SeparatorPrefixTree.h"
#include "StringUtils.h"
FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), root(root) {}
// NOTE: Sadly, we have to do sorting ourselves.
bool FileIgnoreProxy::lessThan(const QModelIndex& left, const QModelIndex& right) const
{
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
if (!fsm) {
return QSortFilterProxyModel::lessThan(left, right);
}
bool asc = sortOrder() == Qt::AscendingOrder ? true : false;
QFileInfo leftFileInfo = fsm->fileInfo(left);
QFileInfo rightFileInfo = fsm->fileInfo(right);
if (!leftFileInfo.isDir() && rightFileInfo.isDir()) {
return !asc;
}
if (leftFileInfo.isDir() && !rightFileInfo.isDir()) {
return asc;
}
// sort and proxy model breaks the original model...
if (sortColumn() == 0) {
return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0;
}
if (sortColumn() == 1) {
auto leftSize = leftFileInfo.size();
auto rightSize = rightFileInfo.size();
if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir())) {
return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0 ? asc : !asc;
}
return leftSize < rightSize;
}
return QSortFilterProxyModel::lessThan(left, right);
}
Qt::ItemFlags FileIgnoreProxy::flags(const QModelIndex& index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
auto sourceIndex = mapToSource(index);
Qt::ItemFlags flags = sourceIndex.flags();
if (index.column() == 0) {
flags |= Qt::ItemIsUserCheckable;
if (sourceIndex.model()->hasChildren(sourceIndex)) {
flags |= Qt::ItemIsAutoTristate;
}
}
return flags;
}
QVariant FileIgnoreProxy::data(const QModelIndex& index, int role) const
{
QModelIndex sourceIndex = mapToSource(index);
if (index.column() == 0 && role == Qt::CheckStateRole) {
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
auto blockedPath = relPath(fsm->filePath(sourceIndex));
auto cover = blocked.cover(blockedPath);
if (!cover.isNull()) {
return QVariant(Qt::Unchecked);
} else if (blocked.exists(blockedPath)) {
return QVariant(Qt::PartiallyChecked);
} else {
return QVariant(Qt::Checked);
}
}
return sourceIndex.data(role);
}
bool FileIgnoreProxy::setData(const QModelIndex& index, const QVariant& value, int role)
{
if (index.column() == 0 && role == Qt::CheckStateRole) {
Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
return setFilterState(index, state);
}
QModelIndex sourceIndex = mapToSource(index);
return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role);
}
QString FileIgnoreProxy::relPath(const QString& path) const
{
return QDir(root).relativeFilePath(path);
}
bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state)
{
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
if (!fsm) {
return false;
}
QModelIndex sourceIndex = mapToSource(index);
auto blockedPath = relPath(fsm->filePath(sourceIndex));
bool changed = false;
if (state == Qt::Unchecked) {
// blocking a path
auto& node = blocked.insert(blockedPath);
// get rid of all blocked nodes below
node.clear();
changed = true;
} else if (state == Qt::Checked || state == Qt::PartiallyChecked) {
if (!blocked.remove(blockedPath)) {
auto cover = blocked.cover(blockedPath);
qDebug() << "Blocked by cover" << cover;
// uncover
blocked.remove(cover);
// block all contents, except for any cover
QModelIndex rootIndex = fsm->index(FS::PathCombine(root, cover));
QModelIndex doing = rootIndex;
int row = 0;
QStack<QModelIndex> todo;
while (1) {
auto node = fsm->index(row, 0, doing);
if (!node.isValid()) {
if (!todo.size()) {
break;
} else {
doing = todo.pop();
row = 0;
continue;
}
}
auto relpath = relPath(fsm->filePath(node));
if (blockedPath.startsWith(relpath)) // cover found?
{
// continue processing cover later
todo.push(node);
} else {
// or just block this one.
blocked.insert(relpath);
}
row++;
}
}
changed = true;
}
if (changed) {
// update the thing
emit dataChanged(index, index, { Qt::CheckStateRole });
// update everything above index
QModelIndex up = index.parent();
while (1) {
if (!up.isValid())
break;
emit dataChanged(up, up, { Qt::CheckStateRole });
up = up.parent();
}
// and everything below the index
QModelIndex doing = index;
int row = 0;
QStack<QModelIndex> todo;
while (1) {
auto node = this->index(row, 0, doing);
if (!node.isValid()) {
if (!todo.size()) {
break;
} else {
doing = todo.pop();
row = 0;
continue;
}
}
emit dataChanged(node, node, { Qt::CheckStateRole });
todo.push(node);
row++;
}
// siblings and unrelated nodes are ignored
}
return true;
}
bool FileIgnoreProxy::shouldExpand(QModelIndex index)
{
QModelIndex sourceIndex = mapToSource(index);
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
if (!fsm) {
return false;
}
auto blockedPath = relPath(fsm->filePath(sourceIndex));
auto found = blocked.find(blockedPath);
if (found) {
return !found->leaf();
}
return false;
}
void FileIgnoreProxy::setBlockedPaths(QStringList paths)
{
beginResetModel();
blocked.clear();
blocked.insert(paths);
endResetModel();
}
bool FileIgnoreProxy::filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const
{
Q_UNUSED(source_parent)
// adjust the columns you want to filter out here
// return false for those that will be hidden
if (source_column == 2 || source_column == 3)
return false;
return true;
}
bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
QFileSystemModel* fsm = qobject_cast<QFileSystemModel*>(sourceModel());
auto fileInfo = fsm->fileInfo(index);
return !ignoreFile(fileInfo);
}
bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const
{
auto fileName = fileInfo.fileName();
auto path = relPath(fileInfo.absoluteFilePath());
return std::any_of(m_ignoreFiles.cbegin(), m_ignoreFiles.cend(), [fileName](auto iFileName) { return fileName == iFileName; }) ||
m_ignoreFilePaths.covers(path);
}
bool FileIgnoreProxy::filterFile(const QString& fileName) const
{
return blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(root), fileName));
}

View File

@ -0,0 +1,85 @@
// 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>
*
* 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 <QFileInfo>
#include <QSortFilterProxyModel>
#include "SeparatorPrefixTree.h"
class FileIgnoreProxy : public QSortFilterProxyModel {
Q_OBJECT
public:
FileIgnoreProxy(QString root, QObject* parent);
// NOTE: Sadly, we have to do sorting ourselves.
bool lessThan(const QModelIndex& left, const QModelIndex& right) const;
virtual Qt::ItemFlags flags(const QModelIndex& index) const;
virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole);
QString relPath(const QString& path) const;
bool setFilterState(QModelIndex index, Qt::CheckState state);
bool shouldExpand(QModelIndex index);
void setBlockedPaths(QStringList paths);
inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; }
inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; }
// list of file names that need to be removed completely from model
inline QStringList& ignoreFilesWithName() { return m_ignoreFiles; }
// list of relative paths that need to be removed completely from model
inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; }
bool filterFile(const QString& fileName) const;
protected:
bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const;
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const;
bool ignoreFile(QFileInfo file) const;
private:
const QString root;
SeparatorPrefixTree<'/'> blocked;
QStringList m_ignoreFiles;
SeparatorPrefixTree<'/'> m_ignoreFilePaths;
};

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -38,8 +40,14 @@
#include "Exception.h"
#include "pathmatcher/IPathMatcher.h"
#include <system_error>
#include <QDir>
#include <QPair>
#include <QFlags>
#include <QLocalServer>
#include <QObject>
#include <QThread>
namespace FS {
@ -75,9 +83,13 @@ bool ensureFilePathExists(QString filenamepath);
*/
bool ensureFolderPathExists(QString filenamepath);
class copy {
/**
* @brief Copies a directory and it's contents from src to dest
*/
class copy : public QObject {
Q_OBJECT
public:
copy(const QString& src, const QString& dst)
copy(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent)
{
m_src.setPath(src);
m_dst.setPath(dst);
@ -87,23 +99,170 @@ class copy {
m_followSymlinks = follow;
return *this;
}
copy& blacklist(const IPathMatcher* filter)
copy& matcher(const IPathMatcher* filter)
{
m_blacklist = filter;
m_matcher = filter;
return *this;
}
bool operator()() { return operator()(QString()); }
copy& whitelist(bool whitelist)
{
m_whitelist = whitelist;
return *this;
}
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalCopied() { return m_copied; }
int totalFailed() { return m_failedPaths.length(); }
QStringList failed() { return m_failedPaths; }
signals:
void fileCopied(const QString& relativeName);
void copyFailed(const QString& relativeName);
// TODO: maybe add a "shouldCopy" signal in the future?
private:
bool operator()(const QString& offset);
bool operator()(const QString& offset, bool dryRun = false);
private:
bool m_followSymlinks = true;
const IPathMatcher* m_blacklist = nullptr;
const IPathMatcher* m_matcher = nullptr;
bool m_whitelist = false;
QDir m_src;
QDir m_dst;
int m_copied;
QStringList m_failedPaths;
};
struct LinkPair {
QString src;
QString dst;
};
struct LinkResult {
QString src;
QString dst;
QString err_msg;
int err_value;
};
class ExternalLinkFileProcess : public QThread {
Q_OBJECT
public:
ExternalLinkFileProcess(QString server, bool useHardLinks, QObject* parent = nullptr)
: QThread(parent), m_useHardLinks(useHardLinks), m_server(server)
{}
void run() override
{
runLinkFile();
emit processExited();
}
signals:
void processExited();
private:
void runLinkFile();
bool m_useHardLinks = false;
QString m_server;
};
/**
* @brief links (a file / a directory and it's contents) from src to dest
*/
class create_link : public QObject {
Q_OBJECT
public:
create_link(const QList<LinkPair> path_pairs, QObject* parent = nullptr) : QObject(parent) { m_path_pairs.append(path_pairs); }
create_link(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent)
{
LinkPair pair = { src, dst };
m_path_pairs.append(pair);
}
create_link& useHardLinks(const bool useHard)
{
m_useHardLinks = useHard;
return *this;
}
create_link& matcher(const IPathMatcher* filter)
{
m_matcher = filter;
return *this;
}
create_link& whitelist(bool whitelist)
{
m_whitelist = whitelist;
return *this;
}
create_link& linkRecursively(bool recursive)
{
m_recursive = recursive;
return *this;
}
create_link& setMaxDepth(int depth)
{
m_max_depth = depth;
return *this;
}
create_link& debug(bool d)
{
m_debug = d;
return *this;
}
std::error_code getOSError() { return m_os_err; }
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalLinked() { return m_linked; }
void runPrivileged() { runPrivileged(QString()); }
void runPrivileged(const QString& offset);
QList<LinkResult> getResults() { return m_path_results; }
signals:
void fileLinked(const QString& srcName, const QString& dstName);
void linkFailed(const QString& srcName, const QString& dstName, const QString& err_msg, int err_value);
void finished();
void finishedPrivileged(bool gotResults);
private:
bool operator()(const QString& offset, bool dryRun = false);
void make_link_list(const QString& offset);
bool make_links();
private:
bool m_useHardLinks = false;
const IPathMatcher* m_matcher = nullptr;
bool m_whitelist = false;
bool m_recursive = true;
/// @brief >= -1 = infinite, 0 = link files at src/* to dest/*, 1 = link files at src/*/* to dest/*/*, etc.
int m_max_depth = -1;
QList<LinkPair> m_path_pairs;
QList<LinkResult> m_path_results;
QList<LinkPair> m_links_to_make;
int m_linked;
bool m_debug = false;
std::error_code m_os_err;
QLocalServer m_linkServer;
};
/**
* @brief moves a file by renaming it
* @param source source file path
* @param dest destination filepath
*
*/
bool move(const QString& source, const QString& dest);
/**
* Delete a folder recursively
*/
@ -112,13 +271,30 @@ bool deletePath(QString path);
/**
* Trash a folder / file
*/
bool trash(QString path, QString *pathInTrash);
bool trash(QString path, QString* pathInTrash = nullptr);
QString PathCombine(const QString& path1, const QString& path2);
QString PathCombine(const QString& path1, const QString& path2, const QString& path3);
QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4);
QString AbsolutePath(QString path);
QString AbsolutePath(const QString& path);
/**
* @brief depth of path. "foo.txt" -> 0 , "bar/foo.txt" -> 1, /baz/bar/foo.txt -> 2, etc.
*
* @param path path to measure
* @return int number of components before base path
*/
int pathDepth(const QString& path);
/**
* @brief cut off segments of path until it is a max of length depth
*
* @param path path to truncate
* @param depth max depth of new path
* @return QString truncated path
*/
QString pathTruncate(const QString& path, int depth);
/**
* Resolve an executable
@ -155,4 +331,203 @@ QString getDesktopDir();
// Overrides one folder with the contents of another, preserving items exclusive to the first folder
// Equivalent to doing QDir::rename, but allowing for overrides
bool overrideFolder(QString overwritten_path, QString override_path);
}
/**
* Creates a shortcut to the specified target file at the specified destination path.
*/
bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon);
enum class FilesystemType {
FAT,
NTFS,
REFS,
EXT,
EXT_2_OLD,
EXT_2_3_4,
XFS,
BTRFS,
NFS,
ZFS,
APFS,
HFS,
HFSPLUS,
HFSX,
FUSEBLK,
F2FS,
UNKNOWN
};
/**
* @brief Ordered Mapping of enum types to reported filesystem names
* this mapping is non exsaustive, it just attempts to capture the filesystems which could be reasonalbly be in use .
* all string values are in uppercase, use `QString.toUpper()` or equivalent during lookup.
*
* QMap is ordered
*
*/
static const QMap<FilesystemType, QStringList> s_filesystem_type_names = {
{FilesystemType::FAT, { "FAT" }},
{FilesystemType::NTFS, { "NTFS" }},
{FilesystemType::REFS, { "REFS" }},
{FilesystemType::EXT_2_OLD, { "EXT_2_OLD", "EXT2_OLD" }},
{FilesystemType::EXT_2_3_4, { "EXT2/3/4", "EXT_2_3_4", "EXT2", "EXT3", "EXT4" }},
{FilesystemType::EXT, { "EXT" }},
{FilesystemType::XFS, { "XFS" }},
{FilesystemType::BTRFS, { "BTRFS" }},
{FilesystemType::NFS, { "NFS" }},
{FilesystemType::ZFS, { "ZFS" }},
{FilesystemType::APFS, { "APFS" }},
{FilesystemType::HFS, { "HFS" }},
{FilesystemType::HFSPLUS, { "HFSPLUS" }},
{FilesystemType::HFSX, { "HFSX" }},
{FilesystemType::FUSEBLK, { "FUSEBLK" }},
{FilesystemType::F2FS, { "F2FS" }},
{FilesystemType::UNKNOWN, { "UNKNOWN" }}
};
/**
* @brief Get the string name of Filesystem enum object
*
* @param type
* @return QString
*/
QString getFilesystemTypeName(FilesystemType type);
/**
* @brief Get the Filesystem enum object from a name
* Does a lookup of the type name and returns an exact match
*
* @param name
* @return FilesystemType
*/
FilesystemType getFilesystemType(const QString& name);
/**
* @brief Get the Filesystem enum object from a name
* Does a fuzzy lookup of the type name and returns an apropreate match
*
* @param name
* @return FilesystemType
*/
FilesystemType getFilesystemTypeFuzzy(const QString& name);
struct FilesystemInfo {
FilesystemType fsType = FilesystemType::UNKNOWN;
QString fsTypeName;
int blockSize;
qint64 bytesAvailable;
qint64 bytesFree;
qint64 bytesTotal;
QString name;
QString rootPath;
};
/**
* @brief path to the near ancestor that exists
*
*/
QString nearestExistentAncestor(const QString& path);
/**
* @brief colect information about the filesystem under a file
*
*/
FilesystemInfo statFS(const QString& path);
static const QList<FilesystemType> s_clone_filesystems = { FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS,
FilesystemType::XFS, FilesystemType::REFS };
/**
* @brief if the Filesystem is reflink/clone capable
*
*/
bool canCloneOnFS(const QString& path);
bool canCloneOnFS(const FilesystemInfo& info);
bool canCloneOnFS(FilesystemType type);
/**
* @brief if the Filesystems are reflink/clone capable and both are on the same device
*
*/
bool canClone(const QString& src, const QString& dst);
/**
* @brief Copies a directory and it's contents from src to dest
*/
class clone : public QObject {
Q_OBJECT
public:
clone(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent)
{
m_src.setPath(src);
m_dst.setPath(dst);
}
clone& matcher(const IPathMatcher* filter)
{
m_matcher = filter;
return *this;
}
clone& whitelist(bool whitelist)
{
m_whitelist = whitelist;
return *this;
}
bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); }
int totalCloned() { return m_cloned; }
int totalFailed() { return m_failedClones.length(); }
QList<QPair<QString, QString>> failed() { return m_failedClones; }
signals:
void fileCloned(const QString& src, const QString& dst);
void cloneFailed(const QString& src, const QString& dst);
private:
bool operator()(const QString& offset, bool dryRun = false);
private:
const IPathMatcher* m_matcher = nullptr;
bool m_whitelist = false;
QDir m_src;
QDir m_dst;
int m_cloned;
QList<QPair<QString, QString>> m_failedClones;
};
/**
* @brief clone/reflink file from src to dst
*
*/
bool clone_file(const QString& src, const QString& dst, std::error_code& ec);
#if defined(Q_OS_WIN)
bool win_ioctl_clone(const std::wstring& src_path, const std::wstring& dst_path, std::error_code& ec);
#elif defined(Q_OS_LINUX)
bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std::error_code& ec);
#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec);
#endif
static const QList<FilesystemType> s_non_link_filesystems = {
FilesystemType::FAT,
};
/**
* @brief if the Filesystem is symlink capable
*
*/
bool canLinkOnFS(const QString& path);
bool canLinkOnFS(const FilesystemInfo& info);
bool canLinkOnFS(FilesystemType type);
/**
* @brief if the Filesystem is symlink capable on both ends
*
*/
bool canLink(const QString& src, const QString& dst);
uintmax_t hardLinkCount(const QString& path);
} // namespace FS

View File

@ -1,76 +0,0 @@
/* 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 <hoedown/html.h>
#include <hoedown/document.h>
#include <QString>
#include <QByteArray>
/**
* hoedown wrapper, because dealing with resource lifetime in C is stupid
*/
class HoeDown
{
public:
class buffer
{
public:
buffer(size_t unit = 4096)
{
buf = hoedown_buffer_new(unit);
}
~buffer()
{
hoedown_buffer_free(buf);
}
const char * cstr()
{
return hoedown_buffer_cstr(buf);
}
void put(QByteArray input)
{
hoedown_buffer_put(buf, reinterpret_cast<uint8_t *>(input.data()), input.size());
}
const uint8_t * data() const
{
return buf->data;
}
size_t size() const
{
return buf->size;
}
hoedown_buffer * buf;
} ib, ob;
HoeDown()
{
renderer = hoedown_html_renderer_new((hoedown_html_flags) 0,0);
document = hoedown_document_new(renderer, (hoedown_extensions) 0, 8);
}
~HoeDown()
{
hoedown_document_free(document);
hoedown_html_renderer_free(renderer);
}
QString process(QByteArray input)
{
ib.put(input);
hoedown_document_render(document, ob.buf, ib.data(), ib.size());
return ob.cstr();
}
private:
hoedown_document * document;
hoedown_renderer * renderer;
};

View File

@ -0,0 +1,194 @@
//
// Created by marcelohdez on 10/22/22.
//
#include "InstanceCopyPrefs.h"
bool InstanceCopyPrefs::allTrue() const
{
return copySaves &&
keepPlaytime &&
copyGameOptions &&
copyResourcePacks &&
copyShaderPacks &&
copyServers &&
copyMods &&
copyScreenshots;
}
// Returns a single RegEx string of the selected folders/files to filter out (ex: ".minecraft/saves|.minecraft/server.dat")
QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const
{
return getSelectedFiltersAsRegex({});
}
QString InstanceCopyPrefs::getSelectedFiltersAsRegex(const QStringList& additionalFilters) const
{
QStringList filters;
if(!copySaves)
filters << "saves";
if(!copyGameOptions)
filters << "options.txt";
if(!copyResourcePacks)
filters << "resourcepacks" << "texturepacks";
if(!copyShaderPacks)
filters << "shaderpacks";
if(!copyServers)
filters << "servers.dat" << "servers.dat_old" << "server-resource-packs";
if(!copyMods)
filters << "coremods" << "mods" << "config";
if(!copyScreenshots)
filters << "screenshots";
for (auto filter : additionalFilters) {
filters << filter;
}
// If we have any filters to add, join them as a single regex string to return:
if (!filters.isEmpty()) {
const QString MC_ROOT = "[.]?minecraft/";
// Ensure first filter starts with root, then join other filters with OR regex before root (ex: ".minecraft/saves|.minecraft/mods"):
return MC_ROOT + filters.join("|" + MC_ROOT);
}
return {};
}
// ======= Getters =======
bool InstanceCopyPrefs::isCopySavesEnabled() const
{
return copySaves;
}
bool InstanceCopyPrefs::isKeepPlaytimeEnabled() const
{
return keepPlaytime;
}
bool InstanceCopyPrefs::isCopyGameOptionsEnabled() const
{
return copyGameOptions;
}
bool InstanceCopyPrefs::isCopyResourcePacksEnabled() const
{
return copyResourcePacks;
}
bool InstanceCopyPrefs::isCopyShaderPacksEnabled() const
{
return copyShaderPacks;
}
bool InstanceCopyPrefs::isCopyServersEnabled() const
{
return copyServers;
}
bool InstanceCopyPrefs::isCopyModsEnabled() const
{
return copyMods;
}
bool InstanceCopyPrefs::isCopyScreenshotsEnabled() const
{
return copyScreenshots;
}
bool InstanceCopyPrefs::isUseSymLinksEnabled() const
{
return useSymLinks;
}
bool InstanceCopyPrefs::isUseHardLinksEnabled() const
{
return useHardLinks;
}
bool InstanceCopyPrefs::isLinkRecursivelyEnabled() const
{
return linkRecursively;
}
bool InstanceCopyPrefs::isDontLinkSavesEnabled() const
{
return dontLinkSaves;
}
bool InstanceCopyPrefs::isUseCloneEnabled() const
{
return useClone;
}
// ======= Setters =======
void InstanceCopyPrefs::enableCopySaves(bool b)
{
copySaves = b;
}
void InstanceCopyPrefs::enableKeepPlaytime(bool b)
{
keepPlaytime = b;
}
void InstanceCopyPrefs::enableCopyGameOptions(bool b)
{
copyGameOptions = b;
}
void InstanceCopyPrefs::enableCopyResourcePacks(bool b)
{
copyResourcePacks = b;
}
void InstanceCopyPrefs::enableCopyShaderPacks(bool b)
{
copyShaderPacks = b;
}
void InstanceCopyPrefs::enableCopyServers(bool b)
{
copyServers = b;
}
void InstanceCopyPrefs::enableCopyMods(bool b)
{
copyMods = b;
}
void InstanceCopyPrefs::enableCopyScreenshots(bool b)
{
copyScreenshots = b;
}
void InstanceCopyPrefs::enableUseSymLinks(bool b)
{
useSymLinks = b;
}
void InstanceCopyPrefs::enableLinkRecursively(bool b)
{
linkRecursively = b;
}
void InstanceCopyPrefs::enableUseHardLinks(bool b)
{
useHardLinks = b;
}
void InstanceCopyPrefs::enableDontLinkSaves(bool b)
{
dontLinkSaves = b;
}
void InstanceCopyPrefs::enableUseClone(bool b)
{
useClone = b;
}

View File

@ -0,0 +1,57 @@
//
// Created by marcelohdez on 10/22/22.
//
#pragma once
#include <QStringList>
struct InstanceCopyPrefs {
public:
[[nodiscard]] bool allTrue() const;
[[nodiscard]] QString getSelectedFiltersAsRegex() const;
[[nodiscard]] QString getSelectedFiltersAsRegex(const QStringList& additionalFilters) const;
// Getters
[[nodiscard]] bool isCopySavesEnabled() const;
[[nodiscard]] bool isKeepPlaytimeEnabled() const;
[[nodiscard]] bool isCopyGameOptionsEnabled() const;
[[nodiscard]] bool isCopyResourcePacksEnabled() const;
[[nodiscard]] bool isCopyShaderPacksEnabled() const;
[[nodiscard]] bool isCopyServersEnabled() const;
[[nodiscard]] bool isCopyModsEnabled() const;
[[nodiscard]] bool isCopyScreenshotsEnabled() const;
[[nodiscard]] bool isUseSymLinksEnabled() const;
[[nodiscard]] bool isLinkRecursivelyEnabled() const;
[[nodiscard]] bool isUseHardLinksEnabled() const;
[[nodiscard]] bool isDontLinkSavesEnabled() const;
[[nodiscard]] bool isUseCloneEnabled() const;
// Setters
void enableCopySaves(bool b);
void enableKeepPlaytime(bool b);
void enableCopyGameOptions(bool b);
void enableCopyResourcePacks(bool b);
void enableCopyShaderPacks(bool b);
void enableCopyServers(bool b);
void enableCopyMods(bool b);
void enableCopyScreenshots(bool b);
void enableUseSymLinks(bool b);
void enableLinkRecursively(bool b);
void enableUseHardLinks(bool b);
void enableDontLinkSaves(bool b);
void enableUseClone(bool b);
protected: // data
bool copySaves = true;
bool keepPlaytime = true;
bool copyGameOptions = true;
bool copyResourcePacks = true;
bool copyShaderPacks = true;
bool copyServers = true;
bool copyMods = true;
bool copyScreenshots = true;
bool useSymLinks = false;
bool linkRecursively = false;
bool useHardLinks = false;
bool dontLinkSaves = false;
bool useClone = false;
};

View File

@ -1,19 +1,34 @@
#include "InstanceCopyTask.h"
#include "settings/INISettingsObject.h"
#include <QDebug>
#include <QtConcurrentRun>
#include "FileSystem.h"
#include "NullInstance.h"
#include "pathmatcher/RegexpMatcher.h"
#include <QtConcurrentRun>
#include "settings/INISettingsObject.h"
InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime)
InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs)
{
m_origInstance = origInstance;
m_keepPlaytime = keepPlaytime;
m_keepPlaytime = prefs.isKeepPlaytimeEnabled();
m_useLinks = prefs.isUseSymLinksEnabled();
m_linkRecursively = prefs.isLinkRecursivelyEnabled();
m_useHardLinks = prefs.isLinkRecursivelyEnabled() && prefs.isUseHardLinksEnabled();
m_copySaves = prefs.isLinkRecursivelyEnabled() && prefs.isDontLinkSavesEnabled() && prefs.isCopySavesEnabled();
m_useClone = prefs.isUseCloneEnabled();
if(!copySaves)
{
QString filters = prefs.getSelectedFiltersAsRegex();
if (m_useLinks || m_useHardLinks) {
if (!filters.isEmpty())
filters += "|";
filters += "instance.cfg";
}
qDebug() << "CopyFilters:" << filters;
if (!filters.isEmpty()) {
// Set regex filter:
// FIXME: get this from the original instance type...
auto matcherReal = new RegexpMatcher("[.]?minecraft/saves");
auto matcherReal = new RegexpMatcher(filters);
matcherReal->caseSensitive(false);
m_matcher.reset(matcherReal);
}
@ -23,10 +38,88 @@ void InstanceCopyTask::executeTask()
{
setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
folderCopy.followSymlinks(false).blacklist(m_matcher.get());
auto copySaves = [&]() {
QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft"));
QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), folderCopy);
QString staging_mc_dir;
if (mcDir.exists() && !dotMCDir.exists())
staging_mc_dir = mcDir.filePath();
else
staging_mc_dir = dotMCDir.filePath();
FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves"));
savesCopy.followSymlinks(true);
return savesCopy();
};
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this, copySaves] {
if (m_useClone) {
FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath);
folderClone.matcher(m_matcher.get());
return folderClone();
} else if (m_useLinks || m_useHardLinks) {
FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath);
int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder
folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get());
bool there_were_errors = false;
if (!folderLink()) {
#if defined Q_OS_WIN32
if (!m_useHardLinks) {
qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks";
qDebug() << "attempting to run with privelage";
QEventLoop loop;
bool got_priv_results = false;
connect(&folderLink, &FS::create_link::finishedPrivileged, this, [&](bool gotResults) {
if (!gotResults) {
qDebug() << "Privileged run exited without results!";
}
got_priv_results = gotResults;
loop.quit();
});
folderLink.runPrivileged();
loop.exec(); // wait for the finished signal
for (auto result : folderLink.getResults()) {
if (result.err_value != 0) {
there_were_errors = true;
}
}
if (m_copySaves) {
there_were_errors |= !copySaves();
}
return got_priv_results && !there_were_errors;
} else {
qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str();
}
#else
qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str();
#endif
return false;
}
if (m_copySaves) {
there_were_errors |= !copySaves();
}
return !there_were_errors;
} else {
FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
folderCopy.followSymlinks(false).matcher(m_matcher.get());
return folderCopy();
}
});
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &InstanceCopyTask::copyFinished);
connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &InstanceCopyTask::copyAborted);
m_copyFutureWatcher.setFuture(m_copyFuture);
@ -35,20 +128,40 @@ void InstanceCopyTask::executeTask()
void InstanceCopyTask::copyFinished()
{
auto successful = m_copyFuture.result();
if(!successful)
{
if (!successful) {
emitFailed(tr("Instance folder copy failed."));
return;
}
// FIXME: shouldn't this be able to report errors?
auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg"));
InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath));
inst->setName(name());
inst->setIconKey(m_instIcon);
if(!m_keepPlaytime) {
if (!m_keepPlaytime) {
inst->resetTimePlayed();
}
if (m_useLinks)
inst->addLinkedInstanceId(m_origInstance->id());
if (m_useLinks) {
auto allowed_symlinks_file = QFileInfo(FS::PathCombine(inst->gameRoot(), "allowed_symlinks.txt"));
QByteArray allowed_symlinks;
if (allowed_symlinks_file.exists()) {
allowed_symlinks.append(FS::read(allowed_symlinks_file.filePath()));
if (allowed_symlinks.right(1) != "\n")
allowed_symlinks.append("\n"); // we want to be on a new line
}
allowed_symlinks.append(m_origInstance->gameRoot().toUtf8());
allowed_symlinks.append("\n");
if (allowed_symlinks_file.isSymLink())
FS::deletePath(allowed_symlinks_file
.filePath()); // we dont want to modify the original. also make sure the resulting file is not itself a link.
FS::write(allowed_symlinks_file.filePath(), allowed_symlinks);
}
emitSucceeded();
}

View File

@ -1,20 +1,21 @@
#pragma once
#include "tasks/Task.h"
#include "net/NetJob.h"
#include <QUrl>
#include <QFuture>
#include <QFutureWatcher>
#include "settings/SettingsObject.h"
#include "BaseVersion.h"
#include <QUrl>
#include "BaseInstance.h"
#include "BaseVersion.h"
#include "InstanceCopyPrefs.h"
#include "InstanceTask.h"
#include "net/NetJob.h"
#include "settings/SettingsObject.h"
#include "tasks/Task.h"
class InstanceCopyTask : public InstanceTask
{
Q_OBJECT
public:
explicit InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime);
explicit InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs);
protected:
//! Entry point for tasks.
@ -22,10 +23,16 @@ protected:
void copyFinished();
void copyAborted();
private: /* data */
private:
/* data */
InstancePtr m_origInstance;
QFuture<bool> m_copyFuture;
QFutureWatcher<bool> m_copyFutureWatcher;
std::unique_ptr<IPathMatcher> m_matcher;
bool m_keepPlaytime;
bool m_useLinks = false;
bool m_useHardLinks = false;
bool m_copySaves = false;
bool m_linkRecursively = false;
bool m_useClone = false;
};

View File

@ -25,9 +25,13 @@ void InstanceCreationTask::executeTask()
return;
qWarning() << "Instance creation failed!";
if (!m_error_message.isEmpty())
if (!m_error_message.isEmpty()) {
qWarning() << "Reason: " << m_error_message;
emitFailed(tr("Error while creating new instance."));
emitFailed(tr("Error while creating new instance:\n%1").arg(m_error_message));
} else {
emitFailed(tr("Error while creating new instance."));
}
return;
}

View File

@ -34,7 +34,7 @@ class InstanceCreationTask : public InstanceTask {
QString getError() const { return m_error_message; }
protected:
void setError(QString message) { m_error_message = message; };
void setError(const QString& message) { m_error_message = message; };
protected:
bool m_abort = false;

View File

@ -41,6 +41,7 @@
#include "MMCZip.h"
#include "NullInstance.h"
#include "QObjectPtr.h"
#include "icons/IconList.h"
#include "icons/IconUtils.h"
@ -55,11 +56,9 @@
#include <quazip/quazipdir.h>
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
{
m_sourceUrl = sourceUrl;
m_parent = parent;
}
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent, QMap<QString, QString>&& extra_info)
: m_sourceUrl(sourceUrl), m_extra_info(extra_info), m_parent(parent)
{}
bool InstanceImportTask::abort()
{
@ -68,7 +67,12 @@ bool InstanceImportTask::abort()
if (m_filesNetJob)
m_filesNetJob->abort();
m_extractFuture.cancel();
if (m_extractFuture.isRunning()) {
// NOTE: The tasks created by QtConcurrent::run() can't actually get cancelled,
// but we can use this call to check the state when the extraction finishes.
m_extractFuture.cancel();
m_extractFuture.waitForFinished();
}
return Task::abort();
}
@ -90,11 +94,12 @@ void InstanceImportTask::executeTask()
entry->setStale(true);
m_archivePath = entry->getFullPath();
m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network());
m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network()));
m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propogateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted);
@ -164,18 +169,14 @@ void InstanceImportTask::processZipPack()
}
else
{
QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg");
QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json");
QStringList paths_to_ignore { "overrides/" };
if (!mmcRoot.isNull())
{
if (QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg", paths_to_ignore); !mmcRoot.isNull()) {
// process as MultiMC instance/pack
qDebug() << "MultiMC:" << mmcRoot;
root = mmcRoot;
m_modpackType = ModpackType::MultiMC;
}
else if(!flameRoot.isNull())
{
} else if (QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json", paths_to_ignore); !flameRoot.isNull()) {
// process as Flame pack
qDebug() << "Flame:" << flameRoot;
root = flameRoot;
@ -191,18 +192,20 @@ void InstanceImportTask::processZipPack()
// make sure we extract just the pack
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), root, extractDir.absolutePath());
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &InstanceImportTask::extractFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &InstanceImportTask::extractAborted);
m_extractFutureWatcher.setFuture(m_extractFuture);
}
void InstanceImportTask::extractFinished()
{
m_packZip.reset();
if (!m_extractFuture.result())
{
if (m_extractFuture.isCanceled())
return;
if (!m_extractFuture.result().has_value()) {
emitFailed(tr("Failed to extract modpack"));
return;
}
QDir extractDir(m_stagingPath);
qDebug() << "Fixing permissions for extracted pack files...";
@ -256,38 +259,54 @@ void InstanceImportTask::extractFinished()
}
}
void InstanceImportTask::extractAborted()
{
emitAborted();
}
void InstanceImportTask::processFlame()
{
auto* inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent);
shared_qobject_ptr<FlameCreationTask> inst_creation_task = nullptr;
if (!m_extra_info.isEmpty()) {
auto pack_id_it = m_extra_info.constFind("pack_id");
Q_ASSERT(pack_id_it != m_extra_info.constEnd());
auto pack_id = pack_id_it.value();
auto pack_version_id_it = m_extra_info.constFind("pack_version_id");
Q_ASSERT(pack_version_id_it != m_extra_info.constEnd());
auto pack_version_id = pack_version_id_it.value();
QString original_instance_id;
auto original_instance_id_it = m_extra_info.constFind("original_instance_id");
if (original_instance_id_it != m_extra_info.constEnd())
original_instance_id = original_instance_id_it.value();
inst_creation_task = makeShared<FlameCreationTask>(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id);
} else {
// FIXME: Find a way to get IDs in directly imported ZIPs
inst_creation_task = makeShared<FlameCreationTask>(m_stagingPath, m_globalSettings, m_parent, QString(), QString());
}
inst_creation_task->setName(*this);
inst_creation_task->setIcon(m_instIcon);
inst_creation_task->setGroup(m_instGroup);
inst_creation_task->setConfirmUpdate(shouldConfirmUpdate());
connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] {
setOverride(inst_creation_task->shouldOverride());
connect(inst_creation_task.get(), &Task::succeeded, this, [this, inst_creation_task] {
setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID());
emitSucceeded();
});
connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress);
connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);
connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed);
connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress);
connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propogateStepProgress);
connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails);
connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort);
connect(inst_creation_task, &Task::aborted, this, &Task::abort);
connect(inst_creation_task, &Task::abortStatusChanged, this, &Task::setAbortable);
connect(this, &Task::aborted, inst_creation_task.get(), &InstanceCreationTask::abort);
connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort);
connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable);
inst_creation_task->start();
}
void InstanceImportTask::processTechnic()
{
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor{ new Technic::TechnicPackProcessor };
connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded);
connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed);
packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath);
@ -327,19 +346,48 @@ void InstanceImportTask::processMultiMC()
void InstanceImportTask::processModrinth()
{
auto* inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, m_sourceUrl.toString());
ModrinthCreationTask* inst_creation_task = nullptr;
if (!m_extra_info.isEmpty()) {
auto pack_id_it = m_extra_info.constFind("pack_id");
Q_ASSERT(pack_id_it != m_extra_info.constEnd());
auto pack_id = pack_id_it.value();
QString pack_version_id;
auto pack_version_id_it = m_extra_info.constFind("pack_version_id");
if (pack_version_id_it != m_extra_info.constEnd())
pack_version_id = pack_version_id_it.value();
QString original_instance_id;
auto original_instance_id_it = m_extra_info.constFind("original_instance_id");
if (original_instance_id_it != m_extra_info.constEnd())
original_instance_id = original_instance_id_it.value();
inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id);
} else {
QString pack_id;
if (!m_sourceUrl.isEmpty()) {
QRegularExpression regex(R"(data\/([^\/]*)\/versions)");
pack_id = regex.match(m_sourceUrl.toString()).captured(1);
}
// FIXME: Find a way to get the ID in directly imported ZIPs
inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id);
}
inst_creation_task->setName(*this);
inst_creation_task->setIcon(m_instIcon);
inst_creation_task->setGroup(m_instGroup);
inst_creation_task->setConfirmUpdate(shouldConfirmUpdate());
connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] {
setOverride(inst_creation_task->shouldOverride());
setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID());
emitSucceeded();
});
connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed);
connect(inst_creation_task, &Task::progress, this, &InstanceImportTask::setProgress);
connect(inst_creation_task, &Task::stepProgress, this, &InstanceImportTask::propogateStepProgress);
connect(inst_creation_task, &Task::status, this, &InstanceImportTask::setStatus);
connect(inst_creation_task, &Task::details, this, &InstanceImportTask::setDetails);
connect(inst_creation_task, &Task::finished, inst_creation_task, &InstanceCreationTask::deleteLater);
connect(this, &Task::aborted, inst_creation_task, &InstanceCreationTask::abort);

View File

@ -56,7 +56,7 @@ class InstanceImportTask : public InstanceTask
{
Q_OBJECT
public:
explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr);
explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr, QMap<QString, QString>&& extra_info = {});
bool abort() override;
const QVector<Flame::File> &getBlockedFiles() const
@ -81,7 +81,6 @@ private slots:
void downloadProgressChanged(qint64 current, qint64 total);
void downloadAborted();
void extractFinished();
void extractAborted();
private: /* data */
NetJob::Ptr m_filesNetJob;
@ -101,6 +100,10 @@ private: /* data */
Modrinth,
} m_modpackType = ModpackType::Unknown;
// Extra info we might need, that's available before, but can't be derived from
// the source URL / the resource it points to alone.
QMap<QString, QString> m_extra_info;
//FIXME: nuke
QWidget* m_parent;
};

View File

@ -129,6 +129,16 @@ QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const
return mimeData;
}
QStringList InstanceList::getLinkedInstancesById(const QString &id) const
{
QStringList linkedInstances;
for (auto inst : m_instances) {
if (inst->isLinkedToInstanceId(id))
linkedInstances.append(inst->id());
}
return linkedInstances;
}
int InstanceList::rowCount(const QModelIndex& parent) const
{
Q_UNUSED(parent);
@ -787,7 +797,9 @@ class InstanceStaging : public Task {
connect(child, &Task::aborted, this, &InstanceStaging::childAborted);
connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable);
connect(child, &Task::status, this, &InstanceStaging::setStatus);
connect(child, &Task::details, this, &InstanceStaging::setDetails);
connect(child, &Task::progress, this, &InstanceStaging::setProgress);
connect(child, &Task::stepProgress, this, &InstanceStaging::propogateStepProgress);
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded);
}
@ -816,7 +828,7 @@ class InstanceStaging : public Task {
void childSucceded()
{
unsigned sleepTime = backoff();
if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, m_child->shouldOverride()))
if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, *m_child.get()))
{
emitSucceeded();
return;
@ -865,7 +877,7 @@ Task* InstanceList::wrapInstanceTask(InstanceTask* task)
QString InstanceList::getStagedInstancePath()
{
QString key = QUuid::createUuid().toString();
QString key = QUuid::createUuid().toString(QUuid::WithoutBraces);
QString tempDir = ".LAUNCHER_TEMP/";
QString relPath = FS::PathCombine(tempDir, key);
QDir rootPath(m_instDir);
@ -880,25 +892,22 @@ QString InstanceList::getStagedInstancePath()
return path;
}
bool InstanceList::commitStagedInstance(const QString& path, InstanceName const& instanceName, const QString& groupName, bool should_override)
bool InstanceList::commitStagedInstance(const QString& path, InstanceName const& instanceName, const QString& groupName, InstanceTask const& commiting)
{
QDir dir;
QString instID;
InstancePtr inst;
auto should_override = commiting.shouldOverride();
if (should_override) {
// This is to avoid problems when the instance folder gets manually renamed
if ((inst = getInstanceByManagedName(instanceName.originalName()))) {
instID = QFileInfo(inst->instanceRoot()).fileName();
} else if ((inst = getInstanceByManagedName(instanceName.modifiedName()))) {
instID = QFileInfo(inst->instanceRoot()).fileName();
} else {
instID = FS::RemoveInvalidFilenameChars(instanceName.modifiedName(), '-');
}
instID = commiting.originalInstanceID();
} else {
instID = FS::DirNameFromString(instanceName.modifiedName(), m_instDir);
}
Q_ASSERT(!instID.isEmpty());
{
WatchLock lock(m_watcher, m_instDir);
QString destination = FS::PathCombine(m_instDir, instID);

View File

@ -133,7 +133,7 @@ public:
* should_override is used when another similar instance already exists, and we want to override it
* - for instance, when updating it.
*/
bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, const QString& groupName, bool should_override);
bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, const QString& groupName, const InstanceTask&);
/**
* Destroy a previously created staging area given by @keyPath - used when creation fails.
@ -154,6 +154,8 @@ public:
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
QStringList getLinkedInstancesById(const QString &id) const;
signals:
void dataIsInvalid();
void instancesChanged();

View File

@ -5,6 +5,7 @@
#include "ui/pages/BasePageProvider.h"
#include "ui/pages/instance/LogPage.h"
#include "ui/pages/instance/VersionPage.h"
#include "ui/pages/instance/ManagedPackPage.h"
#include "ui/pages/instance/ModFolderPage.h"
#include "ui/pages/instance/ResourcePackPage.h"
#include "ui/pages/instance/TexturePackPage.h"
@ -33,10 +34,12 @@ public:
values.append(new LogPage(inst));
std::shared_ptr<MinecraftInstance> onesix = std::dynamic_pointer_cast<MinecraftInstance>(inst);
values.append(new VersionPage(onesix.get()));
values.append(ManagedPackPage::createPage(onesix.get()));
auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList());
modsPage->setFilter("%1 (*.zip *.jar *.litemod)");
modsPage->setFilter("%1 (*.zip *.jar *.litemod *.nilmod)");
values.append(modsPage);
values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList()));
values.append(new NilModFolderPage(onesix.get(), onesix->nilModList()));
values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList()));
values.append(new TexturePackPage(onesix.get(), onesix->texturePackList()));
values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList()));

View File

@ -18,11 +18,37 @@ InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& ol
return InstanceNameChange::ShouldKeep;
}
ShouldUpdate askIfShouldUpdate(QWidget *parent, QString original_version_name)
{
auto info = CustomMessageBox::selectable(
parent, QObject::tr("Similar modpack was found!"),
QObject::tr("One or more of your instances are from this same modpack%1. Do you want to create a "
"separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before "
"updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).")
.arg(original_version_name),
QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort);
info->setButtonText(QMessageBox::Ok, QObject::tr("Update existing instance"));
info->setButtonText(QMessageBox::Abort, QObject::tr("Create new instance"));
info->setButtonText(QMessageBox::Reset, QObject::tr("Cancel"));
info->exec();
if (info->clickedButton() == info->button(QMessageBox::Ok))
return ShouldUpdate::Update;
if (info->clickedButton() == info->button(QMessageBox::Abort))
return ShouldUpdate::SkipUpdating;
return ShouldUpdate::Cancel;
}
QString InstanceName::name() const
{
if (!m_modified_name.isEmpty())
return modifiedName();
return QString("%1 %2").arg(m_original_name, m_original_version);
if (!m_original_version.isEmpty())
return QString("%1 %2").arg(m_original_name, m_original_version);
return m_original_name;
}
QString InstanceName::originalName() const

View File

@ -6,6 +6,8 @@
/* Helpers */
enum class InstanceNameChange { ShouldChange, ShouldKeep };
[[nodiscard]] InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name);
enum class ShouldUpdate { Update, SkipUpdating, Cancel };
[[nodiscard]] ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name);
struct InstanceName {
public:
@ -42,10 +44,20 @@ class InstanceTask : public Task, public InstanceName {
void setGroup(const QString& group) { m_instGroup = group; }
QString group() const { return m_instGroup; }
[[nodiscard]] bool shouldConfirmUpdate() const { return m_confirm_update; }
void setConfirmUpdate(bool confirm) { m_confirm_update = confirm; }
bool shouldOverride() const { return m_override_existing; }
[[nodiscard]] QString originalInstanceID() const { return m_original_instance_id; };
protected:
void setOverride(bool override) { m_override_existing = override; }
void setOverride(bool override, QString instance_id_to_override = {})
{
m_override_existing = override;
if (!instance_id_to_override.isEmpty())
m_original_instance_id = instance_id_to_override;
}
protected: /* data */
SettingsObjectPtr m_globalSettings;
@ -54,4 +66,7 @@ class InstanceTask : public Task, public InstanceName {
QString m_stagingPath;
bool m_override_existing = false;
bool m_confirm_update = true;
QString m_original_instance_id;
};

View File

@ -36,7 +36,7 @@
#include "JavaCommon.h"
#include "java/JavaUtils.h"
#include "ui/dialogs/CustomMessageBox.h"
#include <MMCStrings.h>
#include <QRegularExpression>
bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent)
@ -122,8 +122,7 @@ void JavaCommon::TestCheck::run()
return;
}
checker.reset(new JavaChecker());
connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this,
SLOT(checkFinished(JavaCheckResult)));
connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinished);
checker->m_path = m_path;
checker->performCheck();
}
@ -137,8 +136,7 @@ void JavaCommon::TestCheck::checkFinished(JavaCheckResult result)
return;
}
checker.reset(new JavaChecker());
connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this,
SLOT(checkFinishedWithArgs(JavaCheckResult)));
connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinishedWithArgs);
checker->m_path = m_path;
checker->m_args = m_args;
checker->m_minMem = m_minMem;

View File

@ -112,7 +112,15 @@ void LaunchController::decideAccount()
}
}
m_accountToUse = accounts->defaultAccount();
// Select the account to use. If the instance has a specific account set, that will be used. Otherwise, the default account will be used
auto instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString();
auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId);
if (instanceAccountIndex == -1) {
m_accountToUse = accounts->defaultAccount();
} else {
m_accountToUse = accounts->at(instanceAccountIndex);
}
if (!m_accountToUse)
{
// If no default account is set, ask the user which one to use.
@ -179,8 +187,8 @@ void LaunchController::login() {
switch(m_accountToUse->accountState()) {
case AccountState::Offline: {
m_session->wants_online = false;
// NOTE: fallthrough is intentional
}
/* fallthrough */
case AccountState::Online: {
if(!m_session->wants_online) {
// we ask the user for a player name
@ -259,8 +267,8 @@ void LaunchController::login() {
// This means some sort of soft error that we can fix with a refresh ... so let's refresh.
case AccountState::Unchecked: {
m_accountToUse->refresh();
// NOTE: fallthrough intentional
}
/* fallthrough */
case AccountState::Working: {
// refresh is in progress, we need to wait for it to finish to proceed.
ProgressDialog progDialog(m_parentWidget);
@ -374,15 +382,15 @@ void LaunchController::launchInstance()
}
resolved_servers = resolved_servers + "]\n\n";
}
m_launcher->prependStep(new TextPrint(m_launcher.get(), resolved_servers, MessageLevel::Launcher));
m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), resolved_servers, MessageLevel::Launcher));
} else {
online_mode = m_demo ? "demo" : "offline";
}
m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher));
m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher));
// Prepend Version
m_launcher->prependStep(new TextPrint(m_launcher.get(), BuildConfig.LAUNCHER_DISPLAYNAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher));
m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), BuildConfig.LAUNCHER_DISPLAYNAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher));
m_launcher->start();
}

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Basic start script for running the launcher with the libs packaged with it.
function printerror {

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022,2023 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 flowln <flowlnlnln@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
@ -43,12 +44,8 @@ LoggedProcess::LoggedProcess(QObject *parent) : QProcess(parent)
// QProcess has a strange interface... let's map a lot of those into a few.
connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut);
connect(this, &QProcess::readyReadStandardError, this, &LoggedProcess::on_stdErr);
connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(on_exit(int,QProcess::ExitStatus)));
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
connect(this, SIGNAL(errorOccurred(QProcess::ProcessError)), this, SLOT(on_error(QProcess::ProcessError)));
#else
connect(this, SIGNAL(error(QProcess::ProcessError)), this, SLOT(on_error(QProcess::ProcessError)));
#endif
connect(this, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &LoggedProcess::on_exit);
connect(this, &QProcess::errorOccurred, this, &LoggedProcess::on_error);
connect(this, &QProcess::stateChanged, this, &LoggedProcess::on_stateChange);
}
@ -60,14 +57,23 @@ LoggedProcess::~LoggedProcess()
}
}
QStringList reprocess(const QByteArray& data, QTextDecoder& decoder)
QStringList LoggedProcess::reprocess(const QByteArray& data, QTextDecoder& decoder)
{
auto str = decoder.toUnicode(data);
if (!m_leftover_line.isEmpty()) {
str.prepend(m_leftover_line);
m_leftover_line = "";
}
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed, QString::SkipEmptyParts);
#else
auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed, Qt::SkipEmptyParts);
#endif
if (!str.endsWith(QChar::LineFeed))
m_leftover_line = lines.takeLast();
return lines;
}

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022,2023 Sefa Eyeoglu <contact@scrumplex.net>
*
* 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
@ -88,9 +88,12 @@ private slots:
private:
void changeState(LoggedProcess::State state);
QStringList reprocess(const QByteArray& data, QTextDecoder& decoder);
private:
QTextDecoder m_err_decoder = QTextDecoder(QTextCodec::codecForLocale());
QTextDecoder m_out_decoder = QTextDecoder(QTextCodec::codecForLocale());
QString m_leftover_line;
bool m_killed = false;
State m_state = NotRunning;
int m_exit_code = 0;

View File

@ -1,76 +0,0 @@
#include "MMCStrings.h"
/// TAKEN FROM Qt, because it doesn't expose it intelligently
static inline QChar getNextChar(const QString &s, int location)
{
return (location < s.length()) ? s.at(location) : QChar();
}
/// TAKEN FROM Qt, because it doesn't expose it intelligently
int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs)
{
for (int l1 = 0, l2 = 0; l1 <= s1.count() && l2 <= s2.count(); ++l1, ++l2)
{
// skip spaces, tabs and 0's
QChar c1 = getNextChar(s1, l1);
while (c1.isSpace())
c1 = getNextChar(s1, ++l1);
QChar c2 = getNextChar(s2, l2);
while (c2.isSpace())
c2 = getNextChar(s2, ++l2);
if (c1.isDigit() && c2.isDigit())
{
while (c1.digitValue() == 0)
c1 = getNextChar(s1, ++l1);
while (c2.digitValue() == 0)
c2 = getNextChar(s2, ++l2);
int lookAheadLocation1 = l1;
int lookAheadLocation2 = l2;
int currentReturnValue = 0;
// find the last digit, setting currentReturnValue as we go if it isn't equal
for (QChar lookAhead1 = c1, lookAhead2 = c2;
(lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length());
lookAhead1 = getNextChar(s1, ++lookAheadLocation1),
lookAhead2 = getNextChar(s2, ++lookAheadLocation2))
{
bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit();
bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit();
if (!is1ADigit && !is2ADigit)
break;
if (!is1ADigit)
return -1;
if (!is2ADigit)
return 1;
if (currentReturnValue == 0)
{
if (lookAhead1 < lookAhead2)
{
currentReturnValue = -1;
}
else if (lookAhead1 > lookAhead2)
{
currentReturnValue = 1;
}
}
}
if (currentReturnValue != 0)
return currentReturnValue;
}
if (cs == Qt::CaseInsensitive)
{
if (!c1.isLower())
c1 = c1.toLower();
if (!c2.isLower())
c2 = c2.toLower();
}
int r = QString::localeAwareCompare(c1, c2);
if (r < 0)
return -1;
if (r > 0)
return 1;
}
// The two strings are the same (02 == 2) so fall back to the normal sort
return QString::compare(s1, s2, cs);
}

View File

@ -1,8 +0,0 @@
#pragma once
#include <QString>
namespace Strings
{
int naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs);
}

View File

@ -18,6 +18,8 @@
#include <MMCTime.h>
#include <QObject>
#include <QDateTime>
#include <QTextStream>
QString Time::prettifyDuration(int64_t duration) {
int seconds = (int) (duration % 60);
@ -28,11 +30,73 @@ QString Time::prettifyDuration(int64_t duration) {
int days = (int) (duration / 24);
if((hours == 0)&&(days == 0))
{
return QObject::tr("%1m %2s").arg(minutes).arg(seconds);
return QObject::tr("%1min %2s").arg(minutes).arg(seconds);
}
if (days == 0)
{
return QObject::tr("%1h %2m").arg(hours).arg(minutes);
return QObject::tr("%1h %2min").arg(hours).arg(minutes);
}
return QObject::tr("%1d %2h %3m").arg(days).arg(hours).arg(minutes);
return QObject::tr("%1d %2h %3min").arg(days).arg(hours).arg(minutes);
}
QString Time::humanReadableDuration(double duration, int precision) {
using days = std::chrono::duration<int, std::ratio<86400>>;
QString outStr;
QTextStream os(&outStr);
bool neg = false;
if (duration < 0) {
neg = true; // flag
duration *= -1; // invert
}
auto std_duration = std::chrono::duration<double>(duration);
auto d = std::chrono::duration_cast<days>(std_duration);
std_duration -= d;
auto h = std::chrono::duration_cast<std::chrono::hours>(std_duration);
std_duration -= h;
auto m = std::chrono::duration_cast<std::chrono::minutes>(std_duration);
std_duration -= m;
auto s = std::chrono::duration_cast<std::chrono::seconds>(std_duration);
std_duration -= s;
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(std_duration);
auto dc = d.count();
auto hc = h.count();
auto mc = m.count();
auto sc = s.count();
auto msc = ms.count();
if (neg) {
os << '-';
}
if (dc) {
os << dc << QObject::tr("days");
}
if (hc) {
if (dc)
os << " ";
os << qSetFieldWidth(2) << hc << QObject::tr("h"); // hours
}
if (mc) {
if (dc || hc)
os << " ";
os << qSetFieldWidth(2) << mc << QObject::tr("m"); // minutes
}
if (dc || hc || mc || sc) {
if (dc || hc || mc)
os << " ";
os << qSetFieldWidth(2) << sc << QObject::tr("s"); // seconds
}
if ((msc && (precision > 0)) || !(dc || hc || mc || sc)) {
if (dc || hc || mc || sc)
os << " ";
os << qSetFieldWidth(0) << qSetRealNumberPrecision(precision) << msc << QObject::tr("ms"); // miliseconds
}
os.flush();
return outStr;
}

View File

@ -22,4 +22,13 @@ namespace Time {
QString prettifyDuration(int64_t duration);
/**
* @brief Returns a string with short form time duration ie. `2days 1h3m4s56.0ms`.
* miliseconds are only included if `precision` is greater than 0.
*
* @param duration a number of seconds as floating point
* @param precision number of decmial points to display on fractons of a second, defualts to 0.
* @return QString
*/
QString humanReadableDuration(double duration, int precision = 0);
}

View File

@ -39,6 +39,7 @@
#include "MMCZip.h"
#include "FileSystem.h"
#include <QCoreApplication>
#include <QDebug>
// ours
@ -93,20 +94,28 @@ bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &containe
return true;
}
bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files)
bool MMCZip::compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, bool followSymlinks)
{
QDir directory(dir);
if (!directory.exists()) return false;
for (auto e : files) {
auto filePath = directory.relativeFilePath(e.absoluteFilePath());
if( !JlCompress::compressFile(zip, e.absoluteFilePath(), filePath)) return false;
auto srcPath = e.absoluteFilePath();
if (followSymlinks) {
if (e.isSymLink()) {
srcPath = e.symLinkTarget();
} else {
srcPath = e.canonicalFilePath();
}
}
if( !JlCompress::compressFile(zip, srcPath, filePath)) return false;
}
return true;
}
bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files)
bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks)
{
QuaZip zip(fileCompressed);
QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
@ -115,7 +124,7 @@ bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList
return false;
}
auto result = compressDirFiles(&zip, dir, files);
auto result = compressDirFiles(&zip, dir, files, followSymlinks);
zip.close();
if(zip.getZipError()!=0) {
@ -228,23 +237,27 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
}
// ours
QString MMCZip::findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root)
QString MMCZip::findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths, const QString& root)
{
QuaZipDir rootDir(zip, root);
for(auto fileName: rootDir.entryList(QDir::Files))
{
if(fileName == what)
for (auto&& fileName : rootDir.entryList(QDir::Files)) {
if (fileName == what)
return root;
QCoreApplication::processEvents();
}
for(auto fileName: rootDir.entryList(QDir::Dirs))
{
QString result = findFolderOfFileInZip(zip, what, root + fileName);
if(!result.isEmpty())
{
// Recurse the search to non-ignored subfolders
for (auto&& fileName : rootDir.entryList(QDir::Dirs)) {
if (ignore_paths.contains(fileName))
continue;
QString result = findFolderOfFileInZip(zip, what, ignore_paths, root + fileName);
if (!result.isEmpty())
return result;
}
}
return QString();
return {};
}
// ours
@ -270,7 +283,8 @@ bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & re
// ours
std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
{
QDir directory(target);
auto target_top_dir = QUrl::fromLocalFile(target);
QStringList extracted;
qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target;
@ -289,16 +303,17 @@ std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & su
return std::nullopt;
}
do
{
QString name = zip->getCurrentFileName();
if(!name.startsWith(subdir))
{
do {
QString file_name = zip->getCurrentFileName();
if (!file_name.startsWith(subdir))
continue;
}
name.remove(0, subdir.size());
auto original_name = name;
auto relative_file_name = QDir::fromNativeSeparators(file_name.remove(0, subdir.size()));
auto original_name = relative_file_name;
// Fix subdirs/files ending with a / getting transformed into absolute paths
if (relative_file_name.startsWith('/'))
relative_file_name = relative_file_name.mid(1);
// Fix subdirs/files ending with a / getting transformed into absolute paths
if(name.startsWith('/')){
@ -306,41 +321,40 @@ std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & su
}
// Fix weird "folders with a single file get squashed" thing
QString path;
if(name.contains('/') && !name.endsWith('/')){
path = name.section('/', 0, -2) + "/";
FS::ensureFolderPathExists(FS::PathCombine(target, path));
QString sub_path;
if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) {
sub_path = relative_file_name.section('/', 0, -2) + '/';
FS::ensureFolderPathExists(FS::PathCombine(target, sub_path));
name = name.split('/').last();
relative_file_name = relative_file_name.split('/').last();
}
QString absFilePath;
if(name.isEmpty())
{
absFilePath = directory.absoluteFilePath(name) + "/";
}
else
{
absFilePath = directory.absoluteFilePath(path + name);
QString target_file_path;
if (relative_file_name.isEmpty()) {
target_file_path = target + '/';
} else {
target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name);
if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/'))
target_file_path += '/';
}
//Block potential file traversal issues
if(!absFilePath.startsWith(directory.absolutePath())){
qWarning() << "Potential file traversal issue, for path " << absFilePath << " with base name as " << directory.absolutePath();
continue;
if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) {
qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path" << target;
return std::nullopt;
}
if (!JlCompress::extractFile(zip, "", absFilePath))
{
qWarning() << "Failed to extract file" << original_name << "to" << absFilePath;
if (!JlCompress::extractFile(zip, "", target_file_path)) {
qWarning() << "Failed to extract file" << original_name << "to" << target_file_path;
JlCompress::removeFile(extracted);
return std::nullopt;
}
extracted.append(absFilePath);
QFile::setPermissions(absFilePath, QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser);
extracted.append(target_file_path);
QFile::setPermissions(target_file_path, QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser);
qDebug() << "Extracted file" << name << "to" << absFilePath;
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
} while (zip->goToNextFile());
return extracted;
}

View File

@ -59,18 +59,20 @@ namespace MMCZip
* \param zip target archive
* \param dir directory that will be compressed (to compress with relative paths)
* \param files list of files to compress
* \param followSymlinks should follow symlinks when compressing file data
* \return true for success or false for failure
*/
bool compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files);
bool compressDirFiles(QuaZip *zip, QString dir, QFileInfoList files, bool followSymlinks = false);
/**
* Compress directory, by providing a list of files to compress
* \param fileCompressed target archive file
* \param dir directory that will be compressed (to compress with relative paths)
* \param files list of files to compress
* \param followSymlinks should follow symlinks when compressing file data
* \return true for success or false for failure
*/
bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files);
bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false);
/**
* take a source jar, add mods to it, resulting in target jar
@ -80,9 +82,11 @@ namespace MMCZip
/**
* Find a single file in archive by file name (not path)
*
* \param ignore_paths paths to skip when recursing the search
*
* \return the path prefix where the file is
*/
QString findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root = QString(""));
QString findFolderOfFileInZip(QuaZip * zip, const QString & what, const QStringList& ignore_paths = {}, const QString &root = QString(""));
/**
* Find a multiple files of the same name in archive by file name

136
launcher/MTPixmapCache.h Normal file
View File

@ -0,0 +1,136 @@
#pragma once
#include <QCoreApplication>
#include <QPixmapCache>
#include <QThread>
#include <QTime>
#include <QDebug>
#define GET_TYPE() \
Qt::ConnectionType type; \
if (QThread::currentThread() != QCoreApplication::instance()->thread()) \
type = Qt::BlockingQueuedConnection; \
else \
type = Qt::DirectConnection;
#define DEFINE_FUNC_NO_PARAM(NAME, RET_TYPE) \
static RET_TYPE NAME() \
{ \
RET_TYPE ret; \
GET_TYPE() \
QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret)); \
return ret; \
}
#define DEFINE_FUNC_ONE_PARAM(NAME, RET_TYPE, PARAM_1_TYPE) \
static RET_TYPE NAME(PARAM_1_TYPE p1) \
{ \
RET_TYPE ret; \
GET_TYPE() \
QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1)); \
return ret; \
}
#define DEFINE_FUNC_TWO_PARAM(NAME, RET_TYPE, PARAM_1_TYPE, PARAM_2_TYPE) \
static RET_TYPE NAME(PARAM_1_TYPE p1, PARAM_2_TYPE p2) \
{ \
RET_TYPE ret; \
GET_TYPE() \
QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1), \
Q_ARG(PARAM_2_TYPE, p2)); \
return ret; \
}
/** A wrapper around QPixmapCache with thread affinity with the main thread.
*/
class PixmapCache final : public QObject {
Q_OBJECT
public:
PixmapCache(QObject* parent) : QObject(parent) {}
~PixmapCache() override = default;
static PixmapCache& instance() { return *s_instance; }
static void setInstance(PixmapCache* i) { s_instance = i; }
public:
DEFINE_FUNC_NO_PARAM(cacheLimit, int)
DEFINE_FUNC_NO_PARAM(clear, bool)
DEFINE_FUNC_TWO_PARAM(find, bool, const QString&, QPixmap*)
DEFINE_FUNC_TWO_PARAM(find, bool, const QPixmapCache::Key&, QPixmap*)
DEFINE_FUNC_TWO_PARAM(insert, bool, const QString&, const QPixmap&)
DEFINE_FUNC_ONE_PARAM(insert, QPixmapCache::Key, const QPixmap&)
DEFINE_FUNC_ONE_PARAM(remove, bool, const QString&)
DEFINE_FUNC_ONE_PARAM(remove, bool, const QPixmapCache::Key&)
DEFINE_FUNC_TWO_PARAM(replace, bool, const QPixmapCache::Key&, const QPixmap&)
DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, int)
DEFINE_FUNC_NO_PARAM(markCacheMissByEviciton, bool)
DEFINE_FUNC_ONE_PARAM(setFastEvictionThreshold, bool, int)
// NOTE: Every function returns something non-void to simplify the macros.
private slots:
int _cacheLimit() { return QPixmapCache::cacheLimit(); }
bool _clear()
{
QPixmapCache::clear();
return true;
}
bool _find(const QString& key, QPixmap* pixmap) { return QPixmapCache::find(key, pixmap); }
bool _find(const QPixmapCache::Key& key, QPixmap* pixmap) { return QPixmapCache::find(key, pixmap); }
bool _insert(const QString& key, const QPixmap& pixmap) { return QPixmapCache::insert(key, pixmap); }
QPixmapCache::Key _insert(const QPixmap& pixmap) { return QPixmapCache::insert(pixmap); }
bool _remove(const QString& key)
{
QPixmapCache::remove(key);
return true;
}
bool _remove(const QPixmapCache::Key& key)
{
QPixmapCache::remove(key);
return true;
}
bool _replace(const QPixmapCache::Key& key, const QPixmap& pixmap) { return QPixmapCache::replace(key, pixmap); }
bool _setCacheLimit(int n)
{
QPixmapCache::setCacheLimit(n);
return true;
}
/**
* Mark that a cache miss occurred because of a eviction if too many of these occur too fast the cache size is increased
* @return if the cache size was increased
*/
bool _markCacheMissByEviciton()
{
auto now = QTime::currentTime();
if (!m_last_cache_miss_by_eviciton.isNull()) {
auto diff = m_last_cache_miss_by_eviciton.msecsTo(now);
if (diff < 1000) { // less than a second ago
++m_consecutive_fast_evicitons;
} else {
m_consecutive_fast_evicitons = 0;
}
}
m_last_cache_miss_by_eviciton = now;
if (m_consecutive_fast_evicitons >= m_consecutive_fast_evicitons_threshold) {
// double the cache size
auto newSize = _cacheLimit() * 2;
qDebug() << m_consecutive_fast_evicitons << "pixmap cache misses by eviction happened too fast, doubling cache size to"
<< newSize;
_setCacheLimit(newSize);
m_consecutive_fast_evicitons = 0;
return true;
}
return false;
}
bool _setFastEvictionThreshold(int threshold)
{
m_consecutive_fast_evicitons_threshold = threshold;
return true;
}
private:
static PixmapCache* s_instance;
QTime m_last_cache_miss_by_eviciton;
int m_consecutive_fast_evicitons = 0;
int m_consecutive_fast_evicitons_threshold = 15;
};

109
launcher/MangoHud.cpp Normal file
View File

@ -0,0 +1,109 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PrismLauncher - Minecraft Launcher
* Copyright (C) 2022 Jan Drögehoff <sentrycraft123@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/>.
*/
#include <QStringList>
#include <QDir>
#include <QString>
#include <QSysInfo>
#include <QtGlobal>
#include "MangoHud.h"
#include "FileSystem.h"
#include "Json.h"
namespace MangoHud {
QString getLibraryString()
{
/*
* Check for vulkan layers in this order:
*
* $VK_LAYER_PATH
* $XDG_DATA_DIRS (/usr/local/share/:/usr/share/)
* $XDG_DATA_HOME (~/.local/share)
* /etc
* $XDG_CONFIG_DIRS (/etc/xdg)
* $XDG_CONFIG_HOME (~/.config)
*/
QStringList vkLayerList;
{
QString home = QDir::homePath();
QString vkLayerPath = qEnvironmentVariable("VK_LAYER_PATH");
if (!vkLayerPath.isEmpty()) {
vkLayerList << vkLayerPath;
}
QStringList xdgDataDirs = qEnvironmentVariable("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/").split(QLatin1String(":"));
for (QString dir : xdgDataDirs) {
vkLayerList << FS::PathCombine(dir, "vulkan", "implicit_layer.d");
}
QString xdgDataHome = qEnvironmentVariable("XDG_DATA_HOME");
if (xdgDataHome.isEmpty()) {
xdgDataHome = FS::PathCombine(home, ".local", "share");
}
vkLayerList << FS::PathCombine(xdgDataHome, "vulkan", "implicit_layer.d");
vkLayerList << "/etc";
QStringList xdgConfigDirs = qEnvironmentVariable("XDG_CONFIG_DIRS", "/etc/xdg").split(QLatin1String(":"));
for (QString dir : xdgConfigDirs) {
vkLayerList << FS::PathCombine(dir, "vulkan", "implicit_layer.d");
}
QString xdgConfigHome = qEnvironmentVariable("XDG_CONFIG_HOME");
if (xdgConfigHome.isEmpty()) {
xdgConfigHome = FS::PathCombine(home, ".config");
}
vkLayerList << FS::PathCombine(xdgConfigHome, "vulkan", "implicit_layer.d");
}
for (QString vkLayer : vkLayerList) {
// prefer to use architecture specific vulkan layers
QString currentArch = QSysInfo::currentCpuArchitecture();
if (currentArch == "arm64") {
currentArch = "aarch64";
}
QStringList manifestNames = { QString("MangoHud.%1.json").arg(currentArch), "MangoHud.json" };
QString filePath = "";
for (QString manifestName : manifestNames) {
QString tryPath = FS::PathCombine(vkLayer, manifestName);
if (QFile::exists(tryPath)) {
filePath = tryPath;
break;
}
}
if (filePath.isEmpty()) {
continue;
}
auto conf = Json::requireDocument(filePath, vkLayer);
auto confObject = Json::requireObject(conf, vkLayer);
auto layer = Json::ensureObject(confObject, "layer");
return Json::ensureString(layer, "library_path");
}
return QString();
}
} // namespace MangoHud

27
launcher/MangoHud.h Normal file
View File

@ -0,0 +1,27 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PrismLauncher - Minecraft Launcher
* Copyright (C) 2022 Jan Drögehoff <sentrycraft123@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/>.
*/
#pragma once
#include <QString>
#include <QStringList>
namespace MangoHud {
QString getLibraryString();
}

31
launcher/Markdown.cpp Normal file
View File

@ -0,0 +1,31 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 Joshua Goins <josh@redstrate.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "Markdown.h"
QString markdownToHTML(const QString& markdown)
{
const QByteArray markdownData = markdown.toUtf8();
char* buffer = cmark_markdown_to_html(markdownData.constData(), markdownData.length(), CMARK_OPT_NOBREAKS | CMARK_OPT_UNSAFE);
QString htmlStr(buffer);
free(buffer);
return htmlStr;
}

24
launcher/Markdown.h Normal file
View File

@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 Joshua Goins <josh@redstrate.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/>.
*/
#pragma once
#include <QString>
#include <cmark.h>
QString markdownToHTML(const QString& markdown);

View File

@ -1,72 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ModDownloadTask.h"
#include "Application.h"
#include "minecraft/mod/ModFolderModel.h"
ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed)
: m_mod(mod), m_mod_version(version), mods(mods)
{
if (is_indexed) {
m_update_task.reset(new LocalModUpdateTask(mods->indexDir(), m_mod, m_mod_version));
connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ModDownloadTask::hasOldMod);
addTask(m_update_task);
}
m_filesNetJob.reset(new NetJob(tr("Mod download"), APPLICATION->network()));
m_filesNetJob->setStatus(tr("Downloading mod:\n%1").arg(m_mod_version.downloadUrl));
m_filesNetJob->addNetAction(Net::Download::makeFile(m_mod_version.downloadUrl, mods->dir().absoluteFilePath(getFilename())));
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ModDownloadTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &ModDownloadTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::failed, this, &ModDownloadTask::downloadFailed);
addTask(m_filesNetJob);
}
void ModDownloadTask::downloadSucceeded()
{
m_filesNetJob.reset();
auto name = std::get<0>(to_delete);
auto filename = std::get<1>(to_delete);
if (!name.isEmpty() && filename != m_mod_version.fileName) {
mods->uninstallMod(filename, true);
}
}
void ModDownloadTask::downloadFailed(QString reason)
{
emitFailed(reason);
m_filesNetJob.reset();
}
void ModDownloadTask::downloadProgressChanged(qint64 current, qint64 total)
{
emit progress(current, total);
}
// This indirection is done so that we don't delete a mod before being sure it was
// downloaded successfully!
void ModDownloadTask::hasOldMod(QString name, QString filename)
{
to_delete = {name, filename};
}

View File

@ -1,57 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* 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/>.
*/
#pragma once
#include "net/NetJob.h"
#include "tasks/SequentialTask.h"
#include "modplatform/ModIndex.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
class ModFolderModel;
class ModDownloadTask : public SequentialTask {
Q_OBJECT
public:
explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed = true);
const QString& getFilename() const { return m_mod_version.fileName; }
private:
ModPlatform::IndexedPack m_mod;
ModPlatform::IndexedVersion m_mod_version;
const std::shared_ptr<ModFolderModel> mods;
NetJob::Ptr m_filesNetJob;
LocalModUpdateTask::Ptr m_update_task;
void downloadProgressChanged(qint64 current, qint64 total);
void downloadFailed(QString reason);
void downloadSucceeded();
std::tuple<QString, QString> to_delete {"", ""};
private slots:
void hasOldMod(QString name, QString filename);
};

View File

@ -20,18 +20,34 @@ using unique_qobject_ptr = QScopedPointer<T, QScopedPointerDeleteLater>;
template <typename T>
class shared_qobject_ptr : public QSharedPointer<T> {
public:
constexpr shared_qobject_ptr() : QSharedPointer<T>() {}
constexpr shared_qobject_ptr(T* ptr) : QSharedPointer<T>(ptr, &QObject::deleteLater) {}
constexpr explicit shared_qobject_ptr() : QSharedPointer<T>() {}
constexpr explicit shared_qobject_ptr(T* ptr) : QSharedPointer<T>(ptr, &QObject::deleteLater) {}
constexpr shared_qobject_ptr(std::nullptr_t null_ptr) : QSharedPointer<T>(null_ptr, &QObject::deleteLater) {}
template <typename Derived>
constexpr shared_qobject_ptr(const shared_qobject_ptr<Derived>& other) : QSharedPointer<T>(other)
{}
template <typename Derived>
constexpr shared_qobject_ptr(const QSharedPointer<Derived>& other) : QSharedPointer<T>(other)
{}
void reset() { QSharedPointer<T>::reset(); }
void reset(T*&& other)
{
shared_qobject_ptr<T> t(other);
this->swap(t);
}
void reset(const shared_qobject_ptr<T>& other)
{
shared_qobject_ptr<T> t(other);
this->swap(t);
}
};
template <typename T, typename... Args>
shared_qobject_ptr<T> makeShared(Args... args)
{
auto obj = new T(args...);
return shared_qobject_ptr<T>(obj);
}

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 flowln <flowlnlnln@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
@ -35,32 +36,35 @@
#pragma once
#include "modplatform/ModAPI.h"
#include "ui/pages/modplatform/ModPage.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include <QVariant>
#include <QList>
class ModrinthModPage : public ModPage {
Q_OBJECT
namespace QVariantUtils {
public:
static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
template <typename T>
inline QList<T> toList(QVariant src) {
QVariantList variantList = src.toList();
QList<T> list_t;
list_t.reserve(variantList.size());
for (const QVariant& v : variantList)
{
return ModPage::create<ModrinthModPage>(dialog, instance);
list_t.append(v.value<T>());
}
return list_t;
}
template <typename T>
inline QVariant fromList(QList<T> val) {
QVariantList variantList;
variantList.reserve(val.size());
for (const T& v : val)
{
variantList.append(v);
}
ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance);
~ModrinthModPage() override = default;
return variantList;
}
inline auto displayName() const -> QString override { return "Modrinth"; }
inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); }
inline auto id() const -> QString override { return "modrinth"; }
inline auto helpPage() const -> QString override { return "Mod-platform"; }
inline auto debugName() const -> QString override { return "Modrinth"; }
inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; };
auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
auto shouldDisplay() const -> bool override;
};
}

View File

@ -0,0 +1,92 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ResourceDownloadTask.h"
#include "Application.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ResourceFolderModel.h"
ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion version,
const std::shared_ptr<ResourceFolderModel> packs,
bool is_indexed,
QString custom_target_folder)
: m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs), m_custom_target_folder(custom_target_folder)
{
if (auto model = dynamic_cast<ModFolderModel*>(m_pack_model.get()); model && is_indexed) {
m_update_task.reset(new LocalModUpdateTask(model->indexDir(), *m_pack, m_pack_version));
connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ResourceDownloadTask::hasOldResource);
addTask(m_update_task);
}
m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network()));
m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl));
QDir dir{ m_pack_model->dir() };
{
// FIXME: Make this more generic. May require adding additional info to IndexedVersion,
// or adquiring a reference to the base instance.
if (!m_custom_target_folder.isEmpty()) {
dir.cdUp();
dir.cd(m_custom_target_folder);
}
}
m_filesNetJob->addNetAction(Net::Download::makeFile(m_pack_version.downloadUrl, dir.absoluteFilePath(getFilename())));
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ResourceDownloadTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &ResourceDownloadTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &ResourceDownloadTask::propogateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &ResourceDownloadTask::downloadFailed);
addTask(m_filesNetJob);
}
void ResourceDownloadTask::downloadSucceeded()
{
m_filesNetJob.reset();
auto name = std::get<0>(to_delete);
auto filename = std::get<1>(to_delete);
if (!name.isEmpty() && filename != m_pack_version.fileName) {
if (auto model = dynamic_cast<ModFolderModel*>(m_pack_model.get()); model)
model->uninstallMod(filename, true);
else
m_pack_model->uninstallResource(filename);
}
}
void ResourceDownloadTask::downloadFailed(QString reason)
{
emitFailed(reason);
m_filesNetJob.reset();
}
void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total)
{
emit progress(current, total);
}
// This indirection is done so that we don't delete a mod before being sure it was
// downloaded successfully!
void ResourceDownloadTask::hasOldResource(QString name, QString filename)
{
to_delete = { name, filename };
}

View File

@ -0,0 +1,63 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022-2023 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* 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/>.
*/
#pragma once
#include "net/NetJob.h"
#include "tasks/SequentialTask.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
#include "modplatform/ModIndex.h"
class ResourceFolderModel;
class ResourceDownloadTask : public SequentialTask {
Q_OBJECT
public:
explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion version,
const std::shared_ptr<ResourceFolderModel> packs,
bool is_indexed = true,
QString custom_target_folder = {});
const QString& getFilename() const { return m_pack_version.fileName; }
const QString& getCustomPath() const { return m_custom_target_folder; }
const QVariant& getVersionID() const { return m_pack_version.fileId; }
const ModPlatform::IndexedVersion& getVersion() const { return m_pack_version; }
const ModPlatform::ResourceProvider& getProvider() const { return m_pack->provider; }
const QString& getName() const { return m_pack->name; }
ModPlatform::IndexedPack::Ptr getPack() { return m_pack; }
private:
ModPlatform::IndexedPack::Ptr m_pack;
ModPlatform::IndexedVersion m_pack_version;
const std::shared_ptr<ResourceFolderModel> m_pack_model;
QString m_custom_target_folder;
NetJob::Ptr m_filesNetJob;
LocalModUpdateTask::Ptr m_update_task;
void downloadProgressChanged(qint64 current, qint64 total);
void downloadFailed(QString reason);
void downloadSucceeded();
std::tuple<QString, QString> to_delete{ "", "" };
private slots:
void hasOldResource(QString name, QString filename);
};

184
launcher/StringUtils.cpp Normal file
View File

@ -0,0 +1,184 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 flowln <flowlnlnln@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 "StringUtils.h"
#include <QRegularExpression>
#include <QUuid>
#include <cmath>
/// If you're wondering where these came from exactly, then know you're not the only one =D
/// TAKEN FROM Qt, because it doesn't expose it intelligently
static inline QChar getNextChar(const QString& s, int location)
{
return (location < s.length()) ? s.at(location) : QChar();
}
/// TAKEN FROM Qt, because it doesn't expose it intelligently
int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs)
{
int l1 = 0, l2 = 0;
while (l1 <= s1.count() && l2 <= s2.count()) {
// skip spaces, tabs and 0's
QChar c1 = getNextChar(s1, l1);
while (c1.isSpace())
c1 = getNextChar(s1, ++l1);
QChar c2 = getNextChar(s2, l2);
while (c2.isSpace())
c2 = getNextChar(s2, ++l2);
if (c1.isDigit() && c2.isDigit()) {
while (c1.digitValue() == 0)
c1 = getNextChar(s1, ++l1);
while (c2.digitValue() == 0)
c2 = getNextChar(s2, ++l2);
int lookAheadLocation1 = l1;
int lookAheadLocation2 = l2;
int currentReturnValue = 0;
// find the last digit, setting currentReturnValue as we go if it isn't equal
for (QChar lookAhead1 = c1, lookAhead2 = c2; (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length());
lookAhead1 = getNextChar(s1, ++lookAheadLocation1), lookAhead2 = getNextChar(s2, ++lookAheadLocation2)) {
bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit();
bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit();
if (!is1ADigit && !is2ADigit)
break;
if (!is1ADigit)
return -1;
if (!is2ADigit)
return 1;
if (currentReturnValue == 0) {
if (lookAhead1 < lookAhead2) {
currentReturnValue = -1;
} else if (lookAhead1 > lookAhead2) {
currentReturnValue = 1;
}
}
}
if (currentReturnValue != 0)
return currentReturnValue;
}
if (cs == Qt::CaseInsensitive) {
if (!c1.isLower())
c1 = c1.toLower();
if (!c2.isLower())
c2 = c2.toLower();
}
int r = QString::localeAwareCompare(c1, c2);
if (r < 0)
return -1;
if (r > 0)
return 1;
l1 += 1;
l2 += 1;
}
// The two strings are the same (02 == 2) so fall back to the normal sort
return QString::compare(s1, s2, cs);
}
QString StringUtils::truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_limit)
{
auto display_options = QUrl::RemoveUserInfo | QUrl::RemoveFragment | QUrl::NormalizePathSegments;
auto str_url = url.toDisplayString(display_options);
if (str_url.length() <= max_len)
return str_url;
auto url_path_parts = url.path().split('/');
QString last_path_segment = url_path_parts.takeLast();
if (url_path_parts.size() >= 1 && url_path_parts.first().isEmpty())
url_path_parts.removeFirst(); // drop empty first segment (from leading / )
if (url_path_parts.size() >= 1)
url_path_parts.removeLast(); // drop the next to last path segment
auto url_template = QStringLiteral("%1://%2/%3%4");
auto url_compact = url_path_parts.isEmpty()
? url_template.arg(url.scheme(), url.host(), QStringList({ "...", last_path_segment }).join('/'), url.query())
: url_template.arg(url.scheme(), url.host(),
QStringList({ url_path_parts.join('/'), "...", last_path_segment }).join('/'), url.query());
// remove url parts one by one if it's still too long
while (url_compact.length() > max_len && url_path_parts.size() >= 1) {
url_path_parts.removeLast(); // drop the next to last path segment
url_compact = url_path_parts.isEmpty()
? url_template.arg(url.scheme(), url.host(), QStringList({ "...", last_path_segment }).join('/'), url.query())
: url_template.arg(url.scheme(), url.host(),
QStringList({ url_path_parts.join('/'), "...", last_path_segment }).join('/'), url.query());
}
if ((url_compact.length() >= max_len) && hard_limit) {
// still too long, truncate normaly
url_compact = QString(str_url);
auto to_remove = url_compact.length() - max_len + 3;
url_compact.remove(url_compact.length() - to_remove - 1, to_remove);
url_compact.append("...");
}
return url_compact;
}
static const QStringList s_units_si{ "KB", "MB", "GB", "TB" };
static const QStringList s_units_kibi{ "KiB", "MiB", "GiB", "TiB" };
QString StringUtils::humanReadableFileSize(double bytes, bool use_si, int decimal_points)
{
const QStringList units = use_si ? s_units_si : s_units_kibi;
const int scale = use_si ? 1000 : 1024;
int u = -1;
double r = pow(10, decimal_points);
do {
bytes /= scale;
u++;
} while (round(abs(bytes) * r) / r >= scale && u < units.length() - 1);
return QString::number(bytes, 'f', 2) + " " + units[u];
}
QString StringUtils::getRandomAlphaNumeric()
{
return QUuid::createUuid().toString(QUuid::Id128);
}

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 flowln <flowlnlnln@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
@ -35,33 +36,47 @@
#pragma once
#include "modplatform/ModAPI.h"
#include "ui/pages/modplatform/ModPage.h"
#include <QString>
#include <QUrl>
#include "modplatform/flame/FlameAPI.h"
namespace StringUtils {
class FlameModPage : public ModPage {
Q_OBJECT
#if defined Q_OS_WIN32
using string = std::wstring;
public:
static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
{
return ModPage::create<FlameModPage>(dialog, instance);
}
inline string toStdString(QString s)
{
return s.toStdWString();
}
inline QString fromStdString(string s)
{
return QString::fromStdWString(s);
}
#else
using string = std::string;
FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance);
~FlameModPage() override = default;
inline string toStdString(QString s)
{
return s.toStdString();
}
inline QString fromStdString(string s)
{
return QString::fromStdString(s);
}
#endif
inline auto displayName() const -> QString override { return "CurseForge"; }
inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("flame"); }
inline auto id() const -> QString override { return "curseforge"; }
inline auto helpPage() const -> QString override { return "Mod-platform"; }
int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs);
inline auto debugName() const -> QString override { return "Flame"; }
inline auto metaEntryBase() const -> QString override { return "FlameMods"; };
/**
* @brief Truncate a url while keeping its readability py placing the `...` in the middle of the path
* @param url Url to truncate
* @param max_len max lenght of url in charaters
* @param hard_limit if truncating the path can't get the url short enough, truncate it normaly.
*/
QString truncateUrlHumanFriendly(QUrl &url, int max_len, bool hard_limit = false);
auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override;
bool optedOut(ModPlatform::IndexedVersion& ver) const override;
QString humanReadableFileSize(double bytes, bool use_si = false, int decimal_points = 1);
auto shouldDisplay() const -> bool override;
};
QString getRandomAlphaNumeric();
} // namespace StringUtils

View File

@ -1,443 +0,0 @@
#include <QFile>
#include <QMessageBox>
#include <FileSystem.h>
#include <updater/GoUpdate.h>
#include "UpdateController.h"
#include <QApplication>
#include <thread>
#include <chrono>
#include <LocalPeer.h>
#include "BuildConfig.h"
// from <sys/stat.h>
#ifndef S_IRUSR
#define __S_IREAD 0400 /* Read by owner. */
#define __S_IWRITE 0200 /* Write by owner. */
#define __S_IEXEC 0100 /* Execute by owner. */
#define S_IRUSR __S_IREAD /* Read by owner. */
#define S_IWUSR __S_IWRITE /* Write by owner. */
#define S_IXUSR __S_IEXEC /* Execute by owner. */
#define S_IRGRP (S_IRUSR >> 3) /* Read by group. */
#define S_IWGRP (S_IWUSR >> 3) /* Write by group. */
#define S_IXGRP (S_IXUSR >> 3) /* Execute by group. */
#define S_IROTH (S_IRGRP >> 3) /* Read by others. */
#define S_IWOTH (S_IWGRP >> 3) /* Write by others. */
#define S_IXOTH (S_IXGRP >> 3) /* Execute by others. */
#endif
static QFile::Permissions unixModeToPermissions(const int mode)
{
QFile::Permissions perms;
if (mode & S_IRUSR)
{
perms |= QFile::ReadUser;
}
if (mode & S_IWUSR)
{
perms |= QFile::WriteUser;
}
if (mode & S_IXUSR)
{
perms |= QFile::ExeUser;
}
if (mode & S_IRGRP)
{
perms |= QFile::ReadGroup;
}
if (mode & S_IWGRP)
{
perms |= QFile::WriteGroup;
}
if (mode & S_IXGRP)
{
perms |= QFile::ExeGroup;
}
if (mode & S_IROTH)
{
perms |= QFile::ReadOther;
}
if (mode & S_IWOTH)
{
perms |= QFile::WriteOther;
}
if (mode & S_IXOTH)
{
perms |= QFile::ExeOther;
}
return perms;
}
static const QLatin1String liveCheckFile("live.check");
UpdateController::UpdateController(QWidget * parent, const QString& root, const QString updateFilesDir, GoUpdate::OperationList operations)
{
m_parent = parent;
m_root = root;
m_updateFilesDir = updateFilesDir;
m_operations = operations;
}
void UpdateController::installUpdates()
{
qint64 pid = -1;
QStringList args;
bool started = false;
qDebug() << "Installing updates.";
#ifdef Q_OS_WIN
QString finishCmd = QApplication::applicationFilePath();
#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined (Q_OS_OPENBSD)
QString finishCmd = FS::PathCombine(m_root, BuildConfig.LAUNCHER_NAME);
#elif defined Q_OS_MAC
QString finishCmd = QApplication::applicationFilePath();
#else
#error Unsupported operating system.
#endif
QString backupPath = FS::PathCombine(m_root, "update", "backup");
QDir origin(m_root);
// clean up the backup folder. it should be empty before we start
if(!FS::deletePath(backupPath))
{
qWarning() << "couldn't remove previous backup folder" << backupPath;
}
// and it should exist.
if(!FS::ensureFolderPathExists(backupPath))
{
qWarning() << "couldn't create folder" << backupPath;
return;
}
bool useXPHack = false;
QString exePath;
QString exeOrigin;
QString exeBackup;
// perform the update operations
for(auto op: m_operations)
{
switch(op.type)
{
// replace = move original out to backup, if it exists, move the new file in its place
case GoUpdate::Operation::OP_REPLACE:
{
#ifdef Q_OS_WIN32
QString windowsExeName = BuildConfig.LAUNCHER_NAME + ".exe";
// hack for people renaming the .exe because ... reasons :)
if(op.destination == windowsExeName)
{
op.destination = QFileInfo(QApplication::applicationFilePath()).fileName();
}
#endif
QFileInfo destination (FS::PathCombine(m_root, op.destination));
if(destination.exists())
{
QString backupName = op.destination;
backupName.replace('/', '_');
QString backupFilePath = FS::PathCombine(backupPath, backupName);
if(!QFile::rename(destination.absoluteFilePath(), backupFilePath))
{
qWarning() << "Couldn't move:" << destination.absoluteFilePath() << "to" << backupFilePath;
m_failedOperationType = Replace;
m_failedFile = op.destination;
fail();
return;
}
BackupEntry be;
be.original = destination.absoluteFilePath();
be.backup = backupFilePath;
be.update = op.source;
m_replace_backups.append(be);
}
// make sure the folder we are putting this into exists
if(!FS::ensureFilePathExists(destination.absoluteFilePath()))
{
qWarning() << "REPLACE: Couldn't create folder:" << destination.absoluteFilePath();
m_failedOperationType = Replace;
m_failedFile = op.destination;
fail();
return;
}
// now move the new file in
if(!QFile::rename(op.source, destination.absoluteFilePath()))
{
qWarning() << "REPLACE: Couldn't move:" << op.source << "to" << destination.absoluteFilePath();
m_failedOperationType = Replace;
m_failedFile = op.destination;
fail();
return;
}
QFile::setPermissions(destination.absoluteFilePath(), unixModeToPermissions(op.destinationMode));
}
break;
// delete = move original to backup
case GoUpdate::Operation::OP_DELETE:
{
QString destFilePath = FS::PathCombine(m_root, op.destination);
if(QFile::exists(destFilePath))
{
QString backupName = op.destination;
backupName.replace('/', '_');
QString trashFilePath = FS::PathCombine(backupPath, backupName);
if(!QFile::rename(destFilePath, trashFilePath))
{
qWarning() << "DELETE: Couldn't move:" << op.destination << "to" << trashFilePath;
m_failedFile = op.destination;
m_failedOperationType = Delete;
fail();
return;
}
BackupEntry be;
be.original = destFilePath;
be.backup = trashFilePath;
m_delete_backups.append(be);
}
}
break;
}
}
// try to start the new binary
args = qApp->arguments();
args.removeFirst();
// on old Windows, do insane things... no error checking here, this is just to have something.
if(useXPHack)
{
QString script;
auto nativePath = QDir::toNativeSeparators(exePath);
auto nativeOriginPath = QDir::toNativeSeparators(exeOrigin);
auto nativeBackupPath = QDir::toNativeSeparators(exeBackup);
// so we write this vbscript thing...
QTextStream out(&script);
out << "WScript.Sleep 1000\n";
out << "Set fso=CreateObject(\"Scripting.FileSystemObject\")\n";
out << "Set shell=CreateObject(\"WScript.Shell\")\n";
out << "fso.MoveFile \"" << nativePath << "\", \"" << nativeBackupPath << "\"\n";
out << "fso.MoveFile \"" << nativeOriginPath << "\", \"" << nativePath << "\"\n";
out << "shell.Run \"" << nativePath << "\"\n";
QString scriptPath = FS::PathCombine(m_root, "update", "update.vbs");
// we save it
QFile scriptFile(scriptPath);
scriptFile.open(QIODevice::WriteOnly);
scriptFile.write(script.toLocal8Bit().replace("\n", "\r\n"));
scriptFile.close();
// we run it
started = QProcess::startDetached("wscript", {scriptPath}, m_root);
// and we quit. conscious thought.
qApp->quit();
return;
}
bool doLiveCheck = true;
bool startFailed = false;
// remove live check file, if any
if(QFile::exists(liveCheckFile))
{
if(!QFile::remove(liveCheckFile))
{
qWarning() << "Couldn't remove the" << liveCheckFile << "file! We will proceed without :(";
doLiveCheck = false;
}
}
if(doLiveCheck)
{
if(!args.contains("--alive"))
{
args.append("--alive");
}
}
// FIXME: reparse args and construct a safe variant from scratch. This is a workaround for GH-1874:
QStringList realargs;
int skip = 0;
for(auto & arg: args)
{
if(skip)
{
skip--;
continue;
}
if(arg == "-l")
{
skip = 1;
continue;
}
realargs.append(arg);
}
// start the updated application
started = QProcess::startDetached(finishCmd, realargs, QDir::currentPath(), &pid);
// much dumber check - just find out if the call
if(!started || pid == -1)
{
qWarning() << "Couldn't start new process properly!";
startFailed = true;
}
if(!startFailed && doLiveCheck)
{
int attempts = 0;
while(attempts < 10)
{
attempts++;
QString key;
std::this_thread::sleep_for(std::chrono::milliseconds(250));
if(!QFile::exists(liveCheckFile))
{
qWarning() << "Couldn't find the" << liveCheckFile << "file!";
startFailed = true;
continue;
}
try
{
key = QString::fromUtf8(FS::read(liveCheckFile));
auto id = ApplicationId::fromRawString(key);
LocalPeer peer(nullptr, id);
if(peer.isClient())
{
startFailed = false;
qDebug() << "Found process started with key " << key;
break;
}
else
{
startFailed = true;
qDebug() << "Process started with key " << key << "apparently died or is not reponding...";
break;
}
}
catch (const Exception &e)
{
qWarning() << "Couldn't read the" << liveCheckFile << "file!";
startFailed = true;
continue;
}
}
}
if(startFailed)
{
m_failedOperationType = Start;
fail();
return;
}
else
{
origin.rmdir(m_updateFilesDir);
qApp->quit();
return;
}
}
void UpdateController::fail()
{
qWarning() << "Update failed!";
QString msg;
bool doRollback = false;
QString failTitle = QObject::tr("Update failed!");
QString rollFailTitle = QObject::tr("Rollback failed!");
switch (m_failedOperationType)
{
case Replace:
{
msg = QObject::tr(
"Couldn't replace file %1. Changes will be reverted.\n"
"See the %2 log file for details."
).arg(m_failedFile, BuildConfig.LAUNCHER_DISPLAYNAME);
doRollback = true;
QMessageBox::critical(m_parent, failTitle, msg);
break;
}
case Delete:
{
msg = QObject::tr(
"Couldn't remove file %1. Changes will be reverted.\n"
"See the %2 log file for details."
).arg(m_failedFile, BuildConfig.LAUNCHER_DISPLAYNAME);
doRollback = true;
QMessageBox::critical(m_parent, failTitle, msg);
break;
}
case Start:
{
msg = QObject::tr("The new version didn't start or is too old and doesn't respond to startup checks.\n"
"\n"
"Roll back to previous version?");
auto result = QMessageBox::critical(
m_parent,
failTitle,
msg,
QMessageBox::Yes | QMessageBox::No,
QMessageBox::Yes
);
doRollback = (result == QMessageBox::Yes);
break;
}
case Nothing:
default:
return;
}
if(doRollback)
{
auto rollbackOK = rollback();
if(!rollbackOK)
{
msg = QObject::tr("The rollback failed too.\n"
"You will have to repair %1 manually.\n"
"Please let us know why and how this happened.").arg(BuildConfig.LAUNCHER_DISPLAYNAME);
QMessageBox::critical(m_parent, rollFailTitle, msg);
qApp->quit();
}
}
else
{
qApp->quit();
}
}
bool UpdateController::rollback()
{
bool revertOK = true;
// if the above failed, roll back changes
for(auto backup:m_replace_backups)
{
qWarning() << "restoring" << backup.original << "from" << backup.backup;
if(!QFile::rename(backup.original, backup.update))
{
revertOK = false;
qWarning() << "moving new" << backup.original << "back to" << backup.update << "failed!";
continue;
}
if(!QFile::rename(backup.backup, backup.original))
{
revertOK = false;
qWarning() << "restoring" << backup.original << "failed!";
}
}
for(auto backup:m_delete_backups)
{
qWarning() << "restoring" << backup.original << "from" << backup.backup;
if(!QFile::rename(backup.backup, backup.original))
{
revertOK = false;
qWarning() << "restoring" << backup.original << "failed!";
}
}
return revertOK;
}

View File

@ -1,44 +0,0 @@
#pragma once
#include <QString>
#include <QList>
#include <updater/GoUpdate.h>
class QWidget;
class UpdateController
{
public:
UpdateController(QWidget * parent, const QString &root, const QString updateFilesDir, GoUpdate::OperationList operations);
void installUpdates();
private:
void fail();
bool rollback();
private:
QString m_root;
QString m_updateFilesDir;
GoUpdate::OperationList m_operations;
QWidget * m_parent;
struct BackupEntry
{
// path where we got the new file from
QString update;
// path of what is being actually updated
QString original;
// path where the backup of the updated file was placed
QString backup;
};
QList <BackupEntry> m_replace_backups;
QList <BackupEntry> m_delete_backups;
enum Failure
{
Replace,
Delete,
Start,
Nothing
} m_failedOperationType = Nothing;
QString m_failedFile;
};

View File

@ -1,85 +1,128 @@
#include "Version.h"
#include <QStringList>
#include <QUrl>
#include <QDebug>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QUrl>
Version::Version(const QString &str) : m_string(str)
Version::Version(QString str) : m_string(std::move(str))
{
parse();
}
bool Version::operator<(const Version &other) const
{
const int size = qMax(m_sections.size(), other.m_sections.size());
for (int i = 0; i < size; ++i)
{
const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i);
const Section sec2 =
(i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i);
if (sec1 != sec2)
{
return sec1 < sec2;
}
#define VERSION_OPERATOR(return_on_different) \
bool exclude_our_sections = false; \
bool exclude_their_sections = false; \
\
const auto size = qMax(m_sections.size(), other.m_sections.size()); \
for (int i = 0; i < size; ++i) { \
Section sec1 = (i >= m_sections.size()) ? Section() : m_sections.at(i); \
Section sec2 = (i >= other.m_sections.size()) ? Section() : other.m_sections.at(i); \
\
{ /* Don't include appendixes in the comparison */ \
if (sec1.isAppendix()) \
exclude_our_sections = true; \
if (sec2.isAppendix()) \
exclude_their_sections = true; \
\
if (exclude_our_sections) { \
sec1 = Section(); \
if (sec2.m_isNull) \
break; \
} \
\
if (exclude_their_sections) { \
sec2 = Section(); \
if (sec1.m_isNull) \
break; \
} \
} \
\
if (sec1 != sec2) \
return return_on_different; \
}
bool Version::operator<(const Version& other) const
{
VERSION_OPERATOR(sec1 < sec2)
return false;
}
bool Version::operator<=(const Version &other) const
bool Version::operator==(const Version& other) const
{
return *this < other || *this == other;
}
bool Version::operator>(const Version &other) const
{
const int size = qMax(m_sections.size(), other.m_sections.size());
for (int i = 0; i < size; ++i)
{
const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i);
const Section sec2 =
(i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i);
if (sec1 != sec2)
{
return sec1 > sec2;
}
}
return false;
}
bool Version::operator>=(const Version &other) const
{
return *this > other || *this == other;
}
bool Version::operator==(const Version &other) const
{
const int size = qMax(m_sections.size(), other.m_sections.size());
for (int i = 0; i < size; ++i)
{
const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i);
const Section sec2 =
(i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i);
if (sec1 != sec2)
{
return false;
}
}
VERSION_OPERATOR(false)
return true;
}
bool Version::operator!=(const Version &other) const
bool Version::operator!=(const Version& other) const
{
return !operator==(other);
}
bool Version::operator<=(const Version& other) const
{
return *this < other || *this == other;
}
bool Version::operator>(const Version& other) const
{
return !(*this <= other);
}
bool Version::operator>=(const Version& other) const
{
return !(*this < other);
}
void Version::parse()
{
m_sections.clear();
QString currentSection;
// FIXME: this is bad. versions can contain a lot more separators...
QStringList parts = m_string.split('.');
if (m_string.isEmpty())
return;
for (const auto& part : parts)
{
m_sections.append(Section(part));
auto classChange = [&](QChar lastChar, QChar currentChar) {
if (lastChar.isNull())
return false;
if (lastChar.isDigit() != currentChar.isDigit())
return true;
const QList<QChar> s_separators{ '.', '-', '+' };
if (s_separators.contains(currentChar) && currentSection.at(0) != currentChar)
return true;
return false;
};
currentSection += m_string.at(0);
for (int i = 1; i < m_string.size(); ++i) {
const auto& current_char = m_string.at(i);
if (classChange(m_string.at(i - 1), current_char)) {
if (!currentSection.isEmpty())
m_sections.append(Section(currentSection));
currentSection = "";
}
currentSection += current_char;
}
if (!currentSection.isEmpty())
m_sections.append(Section(currentSection));
}
/// qDebug print support for the Version class
QDebug operator<<(QDebug debug, const Version& v)
{
QDebugStateSaver saver(debug);
debug.nospace() << "Version{ string: " << v.toString() << ", sections: [ ";
bool first = true;
for (auto s : v.m_sections) {
if (!first) debug.nospace() << ", ";
debug.nospace() << s.m_fullString;
first = false;
}
debug.nospace() << " ]" << " }";
return debug;
}

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2023 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
@ -35,17 +36,17 @@
#pragma once
#include <QDebug>
#include <QList>
#include <QString>
#include <QStringView>
#include <QList>
class QUrl;
class Version
{
public:
Version(const QString &str);
Version() {}
class Version {
public:
Version(QString str);
Version() = default;
bool operator<(const Version &other) const;
bool operator<=(const Version &other) const;
@ -54,96 +55,116 @@ public:
bool operator==(const Version &other) const;
bool operator!=(const Version &other) const;
QString toString() const
{
return m_string;
}
QString toString() const { return m_string; }
private:
QString m_string;
struct Section
{
explicit Section(const QString &fullString)
friend QDebug operator<<(QDebug debug, const Version& v);
private:
struct Section {
explicit Section(QString fullString) : m_fullString(std::move(fullString))
{
m_fullString = fullString;
int cutoff = m_fullString.size();
for(int i = 0; i < m_fullString.size(); i++)
{
if(!m_fullString[i].isDigit())
{
for (int i = 0; i < m_fullString.size(); i++) {
if (!m_fullString[i].isDigit()) {
cutoff = i;
break;
}
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
auto numPart = QStringView{m_fullString}.left(cutoff);
#else
auto numPart = m_fullString.leftRef(cutoff);
#endif
if(numPart.size())
{
numValid = true;
if (!numPart.isEmpty()) {
m_isNull = false;
m_numPart = numPart.toInt();
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
auto stringPart = QStringView{m_fullString}.mid(cutoff);
#else
auto stringPart = m_fullString.midRef(cutoff);
#endif
if(stringPart.size())
{
if (!stringPart.isEmpty()) {
m_isNull = false;
m_stringPart = stringPart.toString();
}
}
explicit Section() {}
bool numValid = false;
explicit Section() = default;
bool m_isNull = true;
int m_numPart = 0;
QString m_stringPart;
QString m_fullString;
inline bool operator!=(const Section &other) const
[[nodiscard]] inline bool isAppendix() const { return m_stringPart.startsWith('+'); }
[[nodiscard]] inline bool isPreRelease() const { return m_stringPart.startsWith('-') && m_stringPart.length() > 1; }
inline bool operator==(const Section& other) const
{
if(numValid && other.numValid)
{
return m_numPart != other.m_numPart || m_stringPart != other.m_stringPart;
}
else
{
return m_fullString != other.m_fullString;
if (m_isNull && !other.m_isNull)
return false;
if (!m_isNull && other.m_isNull)
return false;
if (!m_isNull && !other.m_isNull) {
return (m_numPart == other.m_numPart) && (m_stringPart == other.m_stringPart);
}
return true;
}
inline bool operator<(const Section &other) const
{
if(numValid && other.numValid)
{
if(m_numPart < other.m_numPart)
inline bool operator<(const Section& other) const
{
static auto unequal_is_less = [](Section const& non_null) -> bool {
if (non_null.m_stringPart.isEmpty())
return non_null.m_numPart == 0;
return (non_null.m_stringPart != QLatin1Char('.')) && non_null.isPreRelease();
};
if (!m_isNull && other.m_isNull)
return unequal_is_less(*this);
if (m_isNull && !other.m_isNull)
return !unequal_is_less(other);
if (!m_isNull && !other.m_isNull) {
if (m_numPart < other.m_numPart)
return true;
if(m_numPart == other.m_numPart && m_stringPart < other.m_stringPart)
if (m_numPart == other.m_numPart && m_stringPart < other.m_stringPart)
return true;
if (!m_stringPart.isEmpty() && other.m_stringPart.isEmpty())
return false;
if (m_stringPart.isEmpty() && !other.m_stringPart.isEmpty())
return true;
return false;
}
else
{
return m_fullString < other.m_fullString;
}
return m_fullString < other.m_fullString;
}
inline bool operator!=(const Section& other) const
{
return !(*this == other);
}
inline bool operator>(const Section &other) const
{
if(numValid && other.numValid)
{
if(m_numPart > other.m_numPart)
return true;
if(m_numPart == other.m_numPart && m_stringPart > other.m_stringPart)
return true;
return false;
}
else
{
return m_fullString > other.m_fullString;
}
return !(*this < other || *this == other);
}
};
private:
QString m_string;
QList<Section> m_sections;
void parse();
};

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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
@ -54,9 +55,14 @@ public:
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
const auto &filters = m_parent->filters();
const QString &search = m_parent->search();
const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
if (!search.isEmpty() && !sourceModel()->data(idx, BaseVersionList::VersionRole).toString().contains(search, Qt::CaseInsensitive))
return false;
for (auto it = filters.begin(); it != filters.end(); ++it)
{
auto idx = sourceModel()->index(source_row, 0, source_parent);
auto data = sourceModel()->data(idx, it.key());
auto match = data.toString();
if(!it.value()->accepts(match))
@ -187,35 +193,21 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
}
case Qt::ToolTipRole:
{
switch(column)
if(column == Name && hasRecommended)
{
case Name:
auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
if(value.toBool())
{
if(hasRecommended)
return tr("Recommended");
} else if(hasLatest) {
auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
if(value.toBool())
{
auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
if(value.toBool())
{
return tr("Recommended");
}
else if(hasLatest)
{
auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
if(value.toBool())
{
return tr("Latest");
}
}
else if(index.row() == 0)
{
return tr("Latest");
}
return tr("Latest");
}
}
default:
{
return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole);
}
} else {
return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole);
}
}
case Qt::DecorationRole:
@ -239,10 +231,6 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
return APPLICATION->getThemedIcon("bug");
}
}
else if(index.row() == 0)
{
return APPLICATION->getThemedIcon("bug");
}
QPixmap pixmap;
QPixmapCache::find("placeholder", &pixmap);
if(!pixmap)
@ -311,14 +299,14 @@ QModelIndex VersionProxyModel::index(int row, int column, const QModelIndex &par
int VersionProxyModel::columnCount(const QModelIndex &parent) const
{
return m_columns.size();
return parent.isValid() ? 0 : m_columns.size();
}
int VersionProxyModel::rowCount(const QModelIndex &parent) const
{
if(sourceModel())
{
return sourceModel()->rowCount();
return sourceModel()->rowCount(parent);
}
return 0;
}
@ -431,6 +419,7 @@ QModelIndex VersionProxyModel::getVersion(const QString& version) const
void VersionProxyModel::clearFilters()
{
m_filters.clear();
m_search.clear();
filterModel->filterChanged();
}
@ -440,11 +429,21 @@ void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filt
filterModel->filterChanged();
}
void VersionProxyModel::setSearch(const QString &search) {
m_search = search;
filterModel->filterChanged();
}
const VersionProxyModel::FilterMap &VersionProxyModel::filters() const
{
return m_filters;
}
const QString &VersionProxyModel::search() const
{
return m_search;
}
void VersionProxyModel::sourceAboutToBeReset()
{
beginResetModel();

View File

@ -38,7 +38,9 @@ public:
virtual void setSourceModel(QAbstractItemModel *sourceModel) override;
const FilterMap &filters() const;
const QString &search() const;
void setFilter(const BaseVersionList::ModelRoles column, Filter * filter);
void setSearch(const QString &search);
void clearFilters();
QModelIndex getRecommended() const;
QModelIndex getVersion(const QString & version) const;
@ -59,6 +61,7 @@ private slots:
private:
QList<Column> m_columns;
FilterMap m_filters;
QString m_search;
BaseVersionList::RoleList roles;
VersionFilterModel * filterModel;
bool hasRecommended = false;

View File

@ -0,0 +1,277 @@
// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#include "FileLink.h"
#include "BuildConfig.h"
#include "StringUtils.h"
#include <iostream>
#include <QAccessible>
#include <QCommandLineParser>
#include <QDebug>
#include <DesktopServices.h>
#include <sys.h>
#if defined Q_OS_WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <stdio.h>
#include <windows.h>
#endif
// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header
#ifdef __APPLE__
#include <Availability.h> // for deployment target to support pre-catalina targets without std::fs
#endif // __APPLE__
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
#if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
#define GHC_USE_STD_FS
#include <filesystem>
namespace fs = std::filesystem;
#endif // MacOS min version check
#endif // Other OSes version check
#ifndef GHC_USE_STD_FS
#include <ghc/filesystem.hpp>
namespace fs = ghc::filesystem;
#endif
FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this))
{
#if defined Q_OS_WIN32
// attach the parent console
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
// if attach succeeds, reopen and sync all the i/o
if (freopen("CON", "w", stdout)) {
std::cout.sync_with_stdio();
}
if (freopen("CON", "w", stderr)) {
std::cerr.sync_with_stdio();
}
if (freopen("CON", "r", stdin)) {
std::cin.sync_with_stdio();
}
auto out = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD written;
const char* endline = "\n";
WriteConsole(out, endline, strlen(endline), &written, NULL);
consoleAttached = true;
}
#endif
setOrganizationName(BuildConfig.LAUNCHER_NAME);
setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN);
setApplicationName(BuildConfig.LAUNCHER_NAME + "FileLink");
setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT);
// Commandline parsing
QCommandLineParser parser;
parser.setApplicationDescription(QObject::tr("a batch MKLINK program for windows to be used with prismlauncher"));
parser.addOptions({ { { "s", "server" }, "Join the specified server on launch", "pipe name" },
{ { "H", "hard" }, "use hard links instead of symbolic", "true/false" } });
parser.addHelpOption();
parser.addVersionOption();
parser.process(arguments());
QString serverToJoin = parser.value("server");
m_useHardLinks = QVariant(parser.value("hard")).toBool();
qDebug() << "link program launched";
if (!serverToJoin.isEmpty()) {
qDebug() << "joining server" << serverToJoin;
joinServer(serverToJoin);
} else {
qDebug() << "no server to join";
exit();
}
}
void FileLinkApp::joinServer(QString server)
{
blockSize = 0;
in.setDevice(&socket);
connect(&socket, &QLocalSocket::connected, this, [&]() { qDebug() << "connected to server"; });
connect(&socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs);
connect(&socket, &QLocalSocket::errorOccurred, this, [&](QLocalSocket::LocalSocketError socketError) {
switch (socketError) {
case QLocalSocket::ServerNotFoundError:
qDebug()
<< ("The host was not found. Please make sure "
"that the server is running and that the "
"server name is correct.");
break;
case QLocalSocket::ConnectionRefusedError:
qDebug()
<< ("The connection was refused by the peer. "
"Make sure the server is running, "
"and check that the server name "
"is correct.");
break;
case QLocalSocket::PeerClosedError:
qDebug() << ("The connection was closed by the peer. ");
break;
default:
qDebug() << "The following error occurred: " << socket.errorString();
}
});
connect(&socket, &QLocalSocket::disconnected, this, [&]() {
qDebug() << "disconnected from server, should exit";
exit();
});
socket.connectToServer(server);
}
void FileLinkApp::runLink()
{
std::error_code os_err;
qDebug() << "creating links";
for (auto link : m_links_to_make) {
QString src_path = link.src;
QString dst_path = link.dst;
FS::ensureFilePathExists(dst_path);
if (m_useHardLinks) {
qDebug() << "making hard link:" << src_path << "to" << dst_path;
fs::create_hard_link(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err);
} else if (fs::is_directory(StringUtils::toStdString(src_path))) {
qDebug() << "making directory_symlink:" << src_path << "to" << dst_path;
fs::create_directory_symlink(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err);
} else {
qDebug() << "making symlink:" << src_path << "to" << dst_path;
fs::create_symlink(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err);
}
if (os_err) {
qWarning() << "Failed to link files:" << QString::fromStdString(os_err.message());
qDebug() << "Source file:" << src_path;
qDebug() << "Destination file:" << dst_path;
qDebug() << "Error category:" << os_err.category().name();
qDebug() << "Error code:" << os_err.value();
FS::LinkResult result = { src_path, dst_path, QString::fromStdString(os_err.message()), os_err.value() };
m_path_results.append(result);
} else {
FS::LinkResult result = { src_path, dst_path };
m_path_results.append(result);
}
}
sendResults();
qDebug() << "done, should exit soon";
}
void FileLinkApp::sendResults()
{
// construct block of data to send
QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly);
qint32 blocksize = quint32(sizeof(quint32));
for (auto result : m_path_results) {
blocksize += quint32(result.src.size());
blocksize += quint32(result.dst.size());
blocksize += quint32(result.err_msg.size());
blocksize += quint32(sizeof(quint32));
}
qDebug() << "About to write block of size:" << blocksize;
out << blocksize;
out << quint32(m_path_results.length());
for (auto result : m_path_results) {
out << result.src;
out << result.dst;
out << result.err_msg;
out << quint32(result.err_value);
}
qint64 byteswritten = socket.write(block);
bool bytesflushed = socket.flush();
qDebug() << "block flushed" << byteswritten << bytesflushed;
}
void FileLinkApp::readPathPairs()
{
m_links_to_make.clear();
qDebug() << "Reading path pairs from server";
qDebug() << "bytes available" << socket.bytesAvailable();
if (blockSize == 0) {
// Relies on the fact that QDataStream serializes a quint32 into
// sizeof(quint32) bytes
if (socket.bytesAvailable() < (int)sizeof(quint32))
return;
qDebug() << "reading block size";
in >> blockSize;
}
qDebug() << "blocksize is" << blockSize;
qDebug() << "bytes available" << socket.bytesAvailable();
if (socket.bytesAvailable() < blockSize || in.atEnd())
return;
quint32 numLinks;
in >> numLinks;
qDebug() << "numLinks" << numLinks;
for (int i = 0; i < numLinks; i++) {
FS::LinkPair pair;
in >> pair.src;
in >> pair.dst;
qDebug() << "link" << pair.src << "to" << pair.dst;
m_links_to_make.append(pair);
}
runLink();
}
FileLinkApp::~FileLinkApp()
{
qDebug() << "link program shutting down";
// Shut down logger by setting the logger function to nothing
qInstallMessageHandler(nullptr);
#if defined Q_OS_WIN32
// Detach from Windows console
if (consoleAttached) {
fclose(stdout);
fclose(stdin);
fclose(stderr);
FreeConsole();
}
#endif
}

View File

@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#pragma once
#include <QtCore>
#include <QApplication>
#include <QDataStream>
#include <QDateTime>
#include <QDebug>
#include <QFlag>
#include <QIcon>
#include <QLocalSocket>
#include <QUrl>
#include <memory>
#define PRISM_EXTERNAL_EXE
#include "FileSystem.h"
class FileLinkApp : public QCoreApplication {
// friends for the purpose of limiting access to deprecated stuff
Q_OBJECT
public:
FileLinkApp(int& argc, char** argv);
virtual ~FileLinkApp();
private:
void joinServer(QString server);
void readPathPairs();
void runLink();
void sendResults();
bool m_useHardLinks = false;
QDateTime m_startTime;
QLocalSocket socket;
QDataStream in;
quint32 blockSize;
QList<FS::LinkPair> m_links_to_make;
QList<FS::LinkResult> m_path_results;
#if defined Q_OS_WIN32
// used on Windows to attach the standard IO streams
bool consoleAttached = false;
#endif
};

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10, Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
</application>
</compatibility>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel
level="requireAdministrator"
uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>

View File

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
//
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#include "FileLink.h"
int main(int argc, char* argv[])
{
FileLinkApp ldh(argc, argv);
return ldh.exec();
}

View File

@ -66,9 +66,8 @@ IconList::IconList(const QStringList &builtinPaths, QString path, QObject *paren
m_watcher.reset(new QFileSystemWatcher());
is_watching = false;
connect(m_watcher.get(), SIGNAL(directoryChanged(QString)),
SLOT(directoryChanged(QString)));
connect(m_watcher.get(), SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString)));
connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged);
connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged);
directoryChanged(path);
@ -242,7 +241,7 @@ Qt::DropActions IconList::supportedDropActions() const
return Qt::CopyAction;
}
bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex &parent)
{
if (action == Qt::IgnoreAction)
return true;
@ -302,7 +301,7 @@ QVariant IconList::data(const QModelIndex &index, int role) const
int IconList::rowCount(const QModelIndex &parent) const
{
return icons.size();
return parent.isValid() ? 0 : icons.size();
}
void IconList::installIcons(const QStringList &iconFiles)
@ -354,15 +353,18 @@ const MMCIcon *IconList::icon(const QString &key) const
bool IconList::deleteIcon(const QString &key)
{
int iconIdx = getIconIndex(key);
if (iconIdx == -1)
if (!iconFileExists(key))
return false;
auto &iconEntry = icons[iconIdx];
if (iconEntry.has(IconType::FileBased))
{
return QFile::remove(iconEntry.m_images[IconType::FileBased].filename);
}
return false;
return QFile::remove(icon(key)->getFilePath());
}
bool IconList::trashIcon(const QString &key)
{
if (!iconFileExists(key))
return false;
return FS::trash(icon(key)->getFilePath(), nullptr);
}
bool IconList::addThemeIcon(const QString& key)

View File

@ -52,6 +52,7 @@ public:
bool addIcon(const QString &key, const QString &name, const QString &path, const IconType type);
void saveIcon(const QString &key, const QString &path, const char * format) const;
bool deleteIcon(const QString &key);
bool trashIcon(const QString &key);
bool iconFileExists(const QString &key) const;
void installIcons(const QStringList &iconFiles);

View File

@ -85,17 +85,13 @@ void JavaChecker::performCheck()
process->setProgram(m_path);
process->setProcessChannelMode(QProcess::SeparateChannels);
process->setProcessEnvironment(CleanEnviroment());
qDebug() << "Running java checker: " + m_path + args.join(" ");;
qDebug() << "Running java checker:" << m_path << args.join(" ");
connect(process.get(), SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(finished(int, QProcess::ExitStatus)));
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
connect(process.get(), SIGNAL(errorOccurred(QProcess::ProcessError)), this, SLOT(error(QProcess::ProcessError)));
#else
connect(process.get(), SIGNAL(error(QProcess::ProcessError)), this, SLOT(error(QProcess::ProcessError)));
#endif
connect(process.get(), SIGNAL(readyReadStandardOutput()), this, SLOT(stdoutReady()));
connect(process.get(), SIGNAL(readyReadStandardError()), this, SLOT(stderrReady()));
connect(&killTimer, SIGNAL(timeout()), SLOT(timeout()));
connect(process.get(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &JavaChecker::finished);
connect(process.get(), &QProcess::errorOccurred, this, &JavaChecker::error);
connect(process.get(), &QProcess::readyReadStandardOutput, this, &JavaChecker::stdoutReady);
connect(process.get(), &QProcess::readyReadStandardError, this, &JavaChecker::stderrReady);
connect(&killTimer, &QTimer::timeout, this, &JavaChecker::timeout);
killTimer.setSingleShot(true);
killTimer.start(15000);
process->start();
@ -132,7 +128,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status)
result.outLog = m_stdout;
qDebug() << "STDOUT" << m_stdout;
qWarning() << "STDERR" << m_stderr;
qDebug() << "Java checker finished with status " << status << " exit code " << exitcode;
qDebug() << "Java checker finished with status" << status << "exit code" << exitcode;
if (status == QProcess::CrashExit || exitcode == 1)
{

View File

@ -38,7 +38,7 @@ void JavaCheckerJob::executeTask()
for (auto iter : javacheckers)
{
javaresults.append(JavaCheckResult());
connect(iter.get(), SIGNAL(checkFinished(JavaCheckResult)), SLOT(partFinished(JavaCheckResult)));
connect(iter.get(), &JavaChecker::checkFinished, this, &JavaCheckerJob::partFinished);
iter->performCheck();
}
}

View File

@ -1,9 +1,10 @@
#include "JavaInstall.h"
#include <MMCStrings.h>
#include "StringUtils.h"
bool JavaInstall::operator<(const JavaInstall &rhs)
{
auto archCompare = Strings::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive);
auto archCompare = StringUtils::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive);
if(archCompare != 0)
return archCompare < 0;
if(id < rhs.id)
@ -14,7 +15,7 @@ bool JavaInstall::operator<(const JavaInstall &rhs)
{
return false;
}
return Strings::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0;
return StringUtils::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0;
}
bool JavaInstall::operator==(const JavaInstall &rhs)

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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
@ -41,7 +42,6 @@
#include "java/JavaInstallList.h"
#include "java/JavaCheckerJob.h"
#include "java/JavaUtils.h"
#include "MMCStrings.h"
#include "minecraft/VersionFilterData.h"
JavaInstallList::JavaInstallList(QObject *parent) : BaseVersionList(parent)
@ -68,12 +68,12 @@ void JavaInstallList::load()
if(m_status != Status::InProgress)
{
m_status = Status::InProgress;
m_loadTask = new JavaListLoadTask(this);
m_loadTask.reset(new JavaListLoadTask(this));
m_loadTask->start();
}
}
const BaseVersionPtr JavaInstallList::at(int i) const
const BaseVersion::Ptr JavaInstallList::at(int i) const
{
return m_vlist.at(i);
}
@ -99,6 +99,8 @@ QVariant JavaInstallList::data(const QModelIndex &index, int role) const
auto version = std::dynamic_pointer_cast<JavaInstall>(m_vlist[index.row()]);
switch (role)
{
case SortRole:
return -index.row();
case VersionPointerRole:
return QVariant::fromValue(m_vlist[index.row()]);
case VersionIdRole:
@ -122,7 +124,7 @@ BaseVersionList::RoleList JavaInstallList::providesRoles() const
}
void JavaInstallList::updateListData(QList<BaseVersionPtr> versions)
void JavaInstallList::updateListData(QList<BaseVersion::Ptr> versions)
{
beginResetModel();
m_vlist = versions;
@ -137,7 +139,7 @@ void JavaInstallList::updateListData(QList<BaseVersionPtr> versions)
m_loadTask.reset();
}
bool sortJavas(BaseVersionPtr left, BaseVersionPtr right)
bool sortJavas(BaseVersion::Ptr left, BaseVersion::Ptr right)
{
auto rleft = std::dynamic_pointer_cast<JavaInstall>(right);
auto rright = std::dynamic_pointer_cast<JavaInstall>(left);
@ -168,7 +170,7 @@ void JavaListLoadTask::executeTask()
JavaUtils ju;
QList<QString> candidate_paths = ju.FindJavaPaths();
m_job = new JavaCheckerJob("Java detection");
m_job.reset(new JavaCheckerJob("Java detection"));
connect(m_job.get(), &Task::finished, this, &JavaListLoadTask::javaCheckerFinished);
connect(m_job.get(), &Task::progress, this, &Task::setProgress);
@ -210,11 +212,11 @@ void JavaListLoadTask::javaCheckerFinished()
}
}
QList<BaseVersionPtr> javas_bvp;
QList<BaseVersion::Ptr> javas_bvp;
for (auto java : candidates)
{
//qDebug() << java->id << java->arch << " at " << java->path;
BaseVersionPtr bp_java = std::dynamic_pointer_cast<BaseVersion>(java);
BaseVersion::Ptr bp_java = std::dynamic_pointer_cast<BaseVersion>(java);
if (bp_java)
{

View File

@ -42,7 +42,7 @@ public:
Task::Ptr getLoadTask() override;
bool isLoaded() override;
const BaseVersionPtr at(int i) const override;
const BaseVersion::Ptr at(int i) const override;
int count() const override;
void sortVersions() override;
@ -50,7 +50,7 @@ public:
RoleList providesRoles() const override;
public slots:
void updateListData(QList<BaseVersionPtr> versions) override;
void updateListData(QList<BaseVersion::Ptr> versions) override;
protected:
void load();
@ -59,7 +59,7 @@ protected:
protected:
Status m_status = Status::NotDone;
shared_qobject_ptr<JavaListLoadTask> m_loadTask;
QList<BaseVersionPtr> m_vlist;
QList<BaseVersion::Ptr> m_vlist;
};
class JavaListLoadTask : public Task

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